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