1package saml 2 3import ( 4 "bytes" 5 "encoding/xml" 6 "fmt" 7 "time" 8) 9 10const timeFormat = "2006-01-02T15:04:05Z" 11 12type xmlTime time.Time 13 14func (t xmlTime) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { 15 return xml.Attr{ 16 Name: name, 17 Value: time.Time(t).UTC().Format(timeFormat), 18 }, nil 19} 20 21func (t *xmlTime) UnmarshalXMLAttr(attr xml.Attr) error { 22 got, err := time.Parse(timeFormat, attr.Value) 23 if err != nil { 24 return err 25 } 26 *t = xmlTime(got) 27 return nil 28} 29 30type samlVersion struct{} 31 32func (s samlVersion) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { 33 return xml.Attr{ 34 Name: name, 35 Value: "2.0", 36 }, nil 37} 38 39func (s *samlVersion) UnmarshalXMLAttr(attr xml.Attr) error { 40 if attr.Value != "2.0" { 41 return fmt.Errorf(`saml version expected "2.0" got %q`, attr.Value) 42 } 43 return nil 44} 45 46type authnRequest struct { 47 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnRequest"` 48 49 ID string `xml:"ID,attr"` 50 Version samlVersion `xml:"Version,attr"` 51 52 ProviderName string `xml:"ProviderName,attr,omitempty"` 53 IssueInstant xmlTime `xml:"IssueInstant,attr,omitempty"` 54 Consent bool `xml:"Consent,attr,omitempty"` 55 Destination string `xml:"Destination,attr,omitempty"` 56 57 ForceAuthn bool `xml:"ForceAuthn,attr,omitempty"` 58 IsPassive bool `xml:"IsPassive,attr,omitempty"` 59 ProtocolBinding string `xml:"ProtocolBinding,attr,omitempty"` 60 61 AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr,omitempty"` 62 63 Subject *subject `xml:"Subject,omitempty"` 64 Issuer *issuer `xml:"Issuer,omitempty"` 65 NameIDPolicy *nameIDPolicy `xml:"NameIDPolicy,omitempty"` 66 67 // TODO(ericchiang): Make this configurable and determine appropriate default values. 68 RequestAuthnContext *requestAuthnContext `xml:"RequestAuthnContext,omitempty"` 69} 70 71type subject struct { 72 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"` 73 74 NameID *nameID `xml:"NameID,omitempty"` 75 SubjectConfirmations []subjectConfirmation `xml:"SubjectConfirmation"` 76 77 // TODO(ericchiang): Do we need to deal with baseID? 78} 79 80type nameID struct { 81 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"` 82 83 Format string `xml:"Format,omitempty"` 84 Value string `xml:",chardata"` 85} 86 87type subjectConfirmationData struct { 88 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"` 89 90 NotBefore xmlTime `xml:"NotBefore,attr,omitempty"` 91 NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"` 92 Recipient string `xml:"Recipient,attr,omitempty"` 93 InResponseTo string `xml:"InResponseTo,attr,omitempty"` 94} 95 96type subjectConfirmation struct { 97 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmation"` 98 99 Method string `xml:"Method,attr,omitempty"` 100 SubjectConfirmationData *subjectConfirmationData `xml:"SubjectConfirmationData,omitempty"` 101} 102 103type audience struct { 104 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Audience"` 105 Value string `xml:",chardata"` 106} 107 108type audienceRestriction struct { 109 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AudienceRestriction"` 110 111 Audiences []audience `xml:"Audience"` 112} 113 114type conditions struct { 115 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Conditions"` 116 117 NotBefore xmlTime `xml:"NotBefore,attr,omitempty"` 118 NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"` 119 120 AudienceRestriction []audienceRestriction `xml:"AudienceRestriction,omitempty"` 121} 122 123type statusCode struct { 124 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"` 125 126 Value string `xml:"Value,attr,omitempty"` 127} 128 129type statusMessage struct { 130 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusMessage"` 131 132 Value string `xml:",chardata"` 133} 134 135type status struct { 136 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"` 137 138 StatusCode *statusCode `xml:"StatusCode"` 139 StatusMessage *statusMessage `xml:"StatusMessage,omitempty"` 140} 141 142type issuer struct { 143 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"` 144 Issuer string `xml:",chardata"` 145} 146 147type nameIDPolicy struct { 148 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"` 149 AllowCreate bool `xml:"AllowCreate,attr,omitempty"` 150 Format string `xml:"Format,attr,omitempty"` 151} 152 153type requestAuthnContext struct { 154 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol RequestAuthnContext"` 155 156 AuthnContextClassRefs []authnContextClassRef 157} 158 159type authnContextClassRef struct { 160 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnContextClassRef"` 161 Value string `xml:",chardata"` 162} 163 164type response struct { 165 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"` 166 167 ID string `xml:"ID,attr"` 168 InResponseTo string `xml:"InResponseTo,attr"` 169 Version samlVersion `xml:"Version,attr"` 170 171 Destination string `xml:"Destination,attr,omitempty"` 172 173 Issuer *issuer `xml:"Issuer,omitempty"` 174 175 Status *status `xml:"Status"` 176 177 // TODO(ericchiang): How do deal with multiple assertions? 178 Assertion *assertion `xml:"Assertion,omitempty"` 179} 180 181type assertion struct { 182 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"` 183 184 Version samlVersion `xml:"Version,attr"` 185 ID string `xml:"ID,attr"` 186 IssueInstance xmlTime `xml:"IssueInstance,attr"` 187 188 Issuer issuer `xml:"Issuer"` 189 190 Subject *subject `xml:"Subject,omitempty"` 191 192 Conditions *conditions `xml:"Conditions"` 193 194 AttributeStatement *attributeStatement `xml:"AttributeStatement,omitempty"` 195} 196 197type attributeStatement struct { 198 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeStatement"` 199 200 Attributes []attribute `xml:"Attribute"` 201} 202 203func (a *attributeStatement) get(name string) (s string, ok bool) { 204 for _, attr := range a.Attributes { 205 if attr.Name == name { 206 ok = true 207 if len(attr.AttributeValues) > 0 { 208 return attr.AttributeValues[0].Value, true 209 } 210 } 211 } 212 return 213} 214 215func (a *attributeStatement) all(name string) (s []string, ok bool) { 216 for _, attr := range a.Attributes { 217 if attr.Name == name { 218 ok = true 219 for _, val := range attr.AttributeValues { 220 s = append(s, val.Value) 221 } 222 } 223 } 224 return 225} 226 227// names list the names of all attributes in the attribute statement. 228func (a *attributeStatement) names() []string { 229 s := make([]string, len(a.Attributes)) 230 231 for i, attr := range a.Attributes { 232 s[i] = attr.Name 233 } 234 return s 235} 236 237// String is a formatter for logging an attribute statement's sub statements. 238func (a *attributeStatement) String() string { 239 buff := new(bytes.Buffer) 240 for i, attr := range a.Attributes { 241 if i != 0 { 242 buff.WriteString(", ") 243 } 244 buff.WriteString(attr.String()) 245 } 246 return buff.String() 247} 248 249type attribute struct { 250 XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Attribute"` 251 252 Name string `xml:"Name,attr"` 253 254 NameFormat string `xml:"NameFormat,attr,omitempty"` 255 FriendlyName string `xml:"FriendlyName,attr,omitempty"` 256 257 AttributeValues []attributeValue `xml:"AttributeValue,omitempty"` 258} 259 260type attributeValue struct { 261 XMLName xml.Name `xml:"AttributeValue"` 262 Value string `xml:",chardata"` 263} 264 265func (a attribute) String() string { 266 if len(a.AttributeValues) == 1 { 267 // "email" = "jane.doe@coreos.com" 268 return fmt.Sprintf("%q = %q", a.Name, a.AttributeValues[0].Value) 269 } 270 values := make([]string, len(a.AttributeValues)) 271 for i, av := range a.AttributeValues { 272 values[i] = av.Value 273 } 274 275 // "groups" = ["engineering", "docs"] 276 return fmt.Sprintf("%q = %q", a.Name, values) 277} 278