-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathservice.go
329 lines (285 loc) · 10.4 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
package main
import (
"bytes"
"fmt"
"html/template"
"io"
"net/http"
"github.com/italia/spid-go/spidsaml"
)
// This demo application shows how to use the spidsaml package
// This is a stateless object representing your Service Provider. It does
// not hold any information about active sessions, so you can safely store
// it in a global variable.
var sp *spidsaml.SP
// IMPORTANT:
// These variables belong the session of each user. In an actual application
// you would NOT store them as global variables, but you'd store them in the
// user session backed by a cookie, using for example github.com/gorilla/sessions,
// but for simplicity in this example application we are doing this way.
var spidSession *spidsaml.Session
var authnReqID, logoutReqID string
func main() {
// Initialize our SPID object with information about this Service Provider
sp = &spidsaml.SP{
// Required fields
EntityID: "https://spid.comune.roma.it",
KeyFile: "../sample_data/key.pem",
CertFile: "../sample_data/crt.pem",
AssertionConsumerServices: []string{
"http://localhost:8000/spid-sso",
},
// The following fields are only needed for metadata generation
SingleLogoutServices: map[string]spidsaml.SAMLBinding{
"http://localhost:8000/spid-slo": spidsaml.HTTPRedirect,
},
AttributeConsumingServices: []spidsaml.AttributeConsumingService{
{
ServiceName: "Service 1",
Attributes: []string{"fiscalNumber", "name", "familyName", "dateOfBirth"},
},
},
Organization: spidsaml.Organization{
Names: []string{"Foobar"},
DisplayNames: []string{"Foobar"},
URLs: []string{"https://www.foobar.it/"},
},
ContactPerson: spidsaml.ContactPerson{
Email: "[email protected]",
IPACode: "FOOBAR",
},
}
// Load Identity Providers from their XML metadata
err := sp.LoadIDPMetadata("../sample_data/test_idp")
if err != nil {
fmt.Print("Failed to load IdP metadata: ")
fmt.Println(err)
return
}
// Wire routes and endpoints of our example application
http.HandleFunc("/", index)
http.HandleFunc("/metadata", metadata)
http.HandleFunc("/spid-login", spidLogin)
http.HandleFunc("/spid-sso", spidSSO)
http.HandleFunc("/logout", spidLogout)
http.HandleFunc("/spid-slo", spidSLO)
// Dance
fmt.Println("spid-go example application listening on http://localhost:8000")
http.ListenAndServe(":8000", nil)
}
const tmplLayout = `<!DOCTYPE html>
<html lang="en-US">
<head>
<title>spid-go Example Application</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<meta charset="UTF-8" />
</head>
<body>
<div class="container">
<h1>spid-go Example Application</h1>
<div id="content">
{{ . }}
</div>
</div>
</body>
</html>
`
const tmplUser = `<p>This page shows details about the currently logged user.</p>
<p><a class="btn btn-primary" href="/logout">Logout</a></p>
<h1>NameID:</h1>
<p>{{ .NameID }}</p>
<h2>SPID Level:</h2>
<p>{{ .Level }}</p>
<h2>Attributes</h2>
<table>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{{ range $key, $val := .Attributes }}
<tr>
<td>{{ $key }}</td>
<td>{{ $val }}</td>
</tr>
{{ end }}
</table>
`
// If we have an active SPID session, display a page with user attributes,
// otherwise show a generic login page containing the SPID button.
func index(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.New("index").Parse(tmplLayout))
if spidSession == nil {
button := sp.GetButton("/spid-login?idp=%s")
t.Execute(w, template.HTML(button))
} else {
var t2 bytes.Buffer
template.Must(template.New("user").Parse(tmplUser)).Execute(&t2, spidSession)
t.Execute(w, template.HTML(t2.String()))
}
}
// This endpoint exposes our metadata
func metadata(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
io.WriteString(w, sp.Metadata())
}
// This endpoint initiates SSO through the user-chosen Identity Provider.
func spidLogin(w http.ResponseWriter, r *http.Request) {
// Check that we have the mandatory 'idp' parameter and that it matches
// an available Identity Provider.
idp, err := sp.GetIDP(r.URL.Query().Get("idp"))
if err != nil {
http.Error(w, "Invalid IdP selected", http.StatusBadRequest)
return
}
// Craft the AuthnRequest.
authnreq := sp.NewAuthnRequest(idp)
//authnreq.AcsURL = "http://localhost:3000/spid-sso"
authnreq.AcsIndex = 0
authnreq.AttrIndex = 0
authnreq.Level = 1
authnreq.RelayState = "my-relay-state"
// Save the ID of the Authnreq so that we can check it in the response
// in order to prevent forgery.
authnReqID = authnreq.ID
// Uncomment the following lines to use the HTTP-POST binding instead of HTTP-Redirect:
//w.Write(authnreq.PostForm())
//return
// Redirect user to the IdP using its HTTP-Redirect binding.
http.Redirect(w, r, authnreq.RedirectURL(), http.StatusSeeOther)
}
// This endpoint exposes an AssertionConsumerService for our Service Provider.
// During SSO, the Identity Provider will redirect user to this URL POSTing
// the resulting assertion.
func spidSSO(w http.ResponseWriter, r *http.Request) {
// Clear the ID of the outgoing Authnreq, since in this demo we're using a
// global variable for it.
defer func() { authnReqID = "" }()
// Parse and verify the incoming assertion.
r.ParseForm()
response, err := spidsaml.ParseResponse(r, sp)
if err != nil {
fmt.Printf("Bad Response received: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Validate the response, matching the ID of the authentication request
err = response.Validate(authnReqID)
if err != nil {
fmt.Printf("Bad Response received: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// TODO: better error handling:
// - authentication failure
// - authentication cancelled by user
// - temporary server error
// - unavailable SPID level
// Log response as required by the SPID rules.
// Hint: log it in a way that does not mangle whitespace preventing signature from
// being verified at a later time
fmt.Printf("SPID Response: %s\n", response.XML)
if response.Success() {
// Login successful! Initialize our application session and store
// the SPID information for later retrieval.
// TODO: this should be stored in a database instead of the current Dancer
// session, and it should be indexed by SPID SessionID so that we can delete
// it when we get a LogoutRequest from an IdP.
spidSession = response.Session()
// TODO: handle SPID level upgrade:
// - does session ID remain the same? better assume it changes
http.Redirect(w, r, "/", http.StatusSeeOther)
} else {
fmt.Fprintf(w, "Authentication Failed: %s (%s)",
response.StatusMessage(), response.StatusCode2())
}
}
// This endpoint initiates logout.
func spidLogout(w http.ResponseWriter, r *http.Request) {
// If we don't have an open SPID session, do nothing.
if spidSession == nil {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Craft the LogoutRequest.
logoutreq, err := sp.NewLogoutRequest(spidSession)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Save the ID of the LogoutRequest so that we can check it in the response
// in order to prevent forgery.
logoutReqID = logoutreq.ID
// Uncomment the following line to use the HTTP-POST binding instead of HTTP-Redirect:
//w.Write(logoutreq.PostForm())
//return
// Redirect user to the Identity Provider for logout.
http.Redirect(w, r, logoutreq.RedirectURL(), http.StatusSeeOther)
}
// This endpoint exposes a SingleLogoutService for our Service Provider, using
// a HTTP-POST or HTTP-Redirect binding (this package does not support SOAP).
// Identity Providers can direct both LogoutRequest and LogoutResponse messages
// to this endpoint.
func spidSLO(w http.ResponseWriter, r *http.Request) {
if spidSession == nil {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
r.ParseForm()
if (r.Form.Get("SAMLResponse") != "" || r.URL.Query().Get("SAMLResponse") != "") && logoutReqID != "" {
// This is the response to a SP-initiated logout.
// Parse the response and catch validation errors.
response, err := sp.ParseLogoutResponse(r)
if err != nil {
fmt.Printf("Bad LogoutResponse received: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Validate the response, matching the ID of our request
err = response.Validate(r, logoutReqID)
if err != nil {
fmt.Printf("Bad LogoutResponse received: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Logout was successful! Clear the local session.
logoutReqID = ""
spidSession = nil
fmt.Println("Session successfully destroyed.")
// TODO: handle partial logout. Log? Show message to user?
// if (logoutres.Status() == logoutres.Partial) { ... }
// Redirect user back to main page.
http.Redirect(w, r, "/", http.StatusSeeOther)
} else if r.Form.Get("SAMLRequest") != "" || r.URL.Query().Get("SAMLRequest") != "" {
// This is a LogoutRequest (IdP-initiated logout).
logoutreq, err := sp.ParseLogoutRequest(r)
if err != nil {
fmt.Printf("Bad LogoutRequest received: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Now we should retrieve the local session corresponding to the SPID
// session logoutreq.SessionIndex(). However, since we are implementing a HTTP-POST
// binding, this HTTP request comes from the user agent so the current user
// session is automatically the right one. This simplifies things a lot as
// retrieving another session by SPID session ID is tricky without a more
// complex architecture.
status := spidsaml.SuccessLogout
if logoutreq.SessionIndex() == spidSession.SessionIndex {
spidSession = nil
} else {
status = spidsaml.PartialLogout
fmt.Printf("SAML LogoutRequest session (%s) does not match current SPID session (%s)\n",
logoutreq.SessionIndex(), spidSession.SessionIndex)
}
// Craft a LogoutResponse and send it back to the Identity Provider.
logoutres, err := sp.NewLogoutResponse(logoutreq, status)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Redirect user to the Identity Provider for logout.
http.Redirect(w, r, logoutres.RedirectURL(), http.StatusSeeOther)
} else {
http.Error(w, "Invalid request", http.StatusBadRequest)
}
}