server.go 11.7 KB
Newer Older
1
// Package irmaserver is a server allowing IRMA verifiers, issuers or attribute-based signature applications (the requestor) to perform IRMA sessions with irmaclient instances (i.e. the IRMA app). It exposes a RESTful protocol with which the requestor can start and manage the session as well as HTTP endpoints for the irmaclient.
Sietse Ringers's avatar
Sietse Ringers committed
2
package irmaserver
3
4

import (
Sietse Ringers's avatar
Sietse Ringers committed
5
	"crypto/tls"
6
	"crypto/x509"
Sietse Ringers's avatar
Sietse Ringers committed
7
	"encoding/json"
8
	"encoding/pem"
9
	"fmt"
10
11
	"io/ioutil"
	"net/http"
12
	"time"
13

14
	"github.com/dgrijalva/jwt-go"
15
	"github.com/go-chi/chi"
Sietse Ringers's avatar
Sietse Ringers committed
16
	"github.com/go-chi/cors"
17
	"github.com/go-errors/errors"
18
	"github.com/privacybydesign/irmago"
Sietse Ringers's avatar
Sietse Ringers committed
19
20
	"github.com/privacybydesign/irmago/server"
	"github.com/privacybydesign/irmago/server/irmarequestor"
21
22
)

23
var (
24
25
	serv, clientserv *http.Server
	conf             *Configuration
26
)
27

28
// Start the server. If successful then it will not return until Stop() is called.
29
func Start(config *Configuration) error {
30
	if err := Initialize(config); err != nil {
31
32
33
		return err
	}

34
35
36
	bts, _ := json.MarshalIndent(conf, "", "   ")
	conf.Logger.Debug("Configuration: ", string(bts), "\n")

37
38
39
	// Start server(s)
	if conf.separateClientServer() {
		go startClientServer()
40
	}
41
	startRequestorServer()
42

43
	return nil
44
45
}

46
47
func startRequestorServer() {
	serv = &http.Server{}
Sietse Ringers's avatar
Sietse Ringers committed
48
49
	tlsConf, _ := conf.tlsConfig()
	startServer(serv, Handler(), "Server", conf.ListenAddress, conf.Port, tlsConf)
50
51
}

52
53
func startClientServer() {
	clientserv = &http.Server{}
Sietse Ringers's avatar
Sietse Ringers committed
54
55
	tlsConf, _ := conf.clientTlsConfig()
	startServer(clientserv, ClientHandler(), "Client server", conf.ClientListenAddress, conf.ClientPort, tlsConf)
56
57
}

Sietse Ringers's avatar
Sietse Ringers committed
58
func startServer(s *http.Server, handler http.Handler, name, addr string, port int, tlsConf *tls.Config) {
59
60
61
62
	fulladdr := fmt.Sprintf("%s:%d", addr, port)
	conf.Logger.Info(name, " listening at ", fulladdr)
	s.Addr = fulladdr
	s.Handler = handler
Sietse Ringers's avatar
Sietse Ringers committed
63
64
65
	var err error
	if tlsConf != nil {
		s.TLSConfig = tlsConf
66
67
68
		// Disable HTTP/2 (see package documentation of http): it breaks server side events :(
		s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
		conf.Logger.Info(name, " TLS enabled")
Sietse Ringers's avatar
Sietse Ringers committed
69
70
71
72
73
		err = s.ListenAndServeTLS("", "")
	} else {
		err = s.ListenAndServe()
	}
	if err != http.ErrServerClosed {
74
75
76
77
78
79
80
81
82
83
84
85
86
87
		_ = server.LogFatal(err)
	}
}

func Stop() error {
	var err1, err2 error

	// Even if closing serv fails, we want to try closing clientserv
	err1 = serv.Close()
	if clientserv != nil {
		err2 = clientserv.Close()
	}

	// Now check errors
88
	if err1 != nil && err1 != http.ErrServerClosed {
89
90
		return err1
	}
91
	if err2 != nil && err2 != http.ErrServerClosed {
92
93
94
95
96
97
		return err2
	}
	return nil
}

func Initialize(config *Configuration) error {
98
	conf = config
99
	if err := irmarequestor.Initialize(conf.Configuration); err != nil {
100
		return err
101
	}
102
	if err := conf.initialize(); err != nil {
103
		return err
104
	}
105
106
	return nil
}
107

108
109
110
111
112
113
var corsOptions = cors.Options{
	AllowedOrigins: []string{"*"},
	AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Cache-Control"},
	AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
}

114
func ClientHandler() http.Handler {
115
	router := chi.NewRouter()
116
	router.Use(cors.New(corsOptions).Handler)
Sietse Ringers's avatar
Sietse Ringers committed
117

118
	router.Mount("/irma/", irmarequestor.HttpHandlerFunc())
119
120
121
122
123
124
125
	return router
}

// Handler returns a http.Handler that handles all IRMA requestor messages
// and IRMA client messages.
func Handler() http.Handler {
	router := chi.NewRouter()
126
	router.Use(cors.New(corsOptions).Handler)
127
128
129
130
131

	if !conf.separateClientServer() {
		// Mount server for irmaclient
		router.Mount("/irma/", irmarequestor.HttpHandlerFunc())
	}
132

133
	// Server routes
134
	router.Post("/session", handleCreate)
135
	router.Delete("/session/{token}", handleDelete)
136
	router.Get("/session/{token}/status", handleStatus)
137
	router.Get("/session/{token}/statusevents", handleStatusEvents)
138
	router.Get("/session/{token}/result", handleResult)
Sietse Ringers's avatar
Sietse Ringers committed
139
140

	// Routes for getting signed JWTs containing the session result. Only work if configuration has a private key
141
142
	router.Get("/session/{token}/result-jwt", handleJwtResult)
	router.Get("/session/{token}/getproof", handleJwtProofs) // irma_api_server-compatible JWT
143

144
145
	router.Get("/publickey", handlePublicKey)

146
	return router
147
148
}

149
150
151
func handleCreate(w http.ResponseWriter, r *http.Request) {
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
152
153
		conf.Logger.Error("Could not read session request HTTP POST body")
		_ = server.LogError(err)
Sietse Ringers's avatar
Sietse Ringers committed
154
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
155
156
		return
	}
157

Sietse Ringers's avatar
Sietse Ringers committed
158
159
160
	// Authenticate request: check if the requestor is known and allowed to submit requests.
	// We do this by feeding the HTTP POST details to all known authenticators, and see if
	// one of them is applicable and able to authenticate the request.
161
	var (
162
		rrequest  irma.RequestorRequest
163
164
165
166
167
168
		request   irma.SessionRequest
		requestor string
		rerr      *irma.RemoteError
		applies   bool
	)
	for _, authenticator := range authenticators {
169
		applies, rrequest, requestor, rerr = authenticator.Authenticate(r.Header, body)
170
171
172
173
174
		if applies || rerr != nil {
			break
		}
	}
	if rerr != nil {
175
		_ = server.LogError(rerr)
176
177
178
179
		server.WriteResponse(w, nil, rerr)
		return
	}
	if !applies {
180
181
		conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s",
			server.ToJson(r.Header), string(body))
Sietse Ringers's avatar
Sietse Ringers committed
182
		server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
183
184
185
		return
	}

186
187
	// Authorize request: check if the requestor is allowed to verify or issue
	// the requested attributes or credentials
188
	request = rrequest.SessionRequest()
189
190
191
	if request.Action() == irma.ActionIssuing {
		allowed, reason := conf.CanIssue(requestor, request.(*irma.IssuanceRequest).Credentials)
		if !allowed {
192
193
			conf.Logger.Warn("Requestor %s tried to issue credential %s but it is not authorized to; full request: %s",
				requestor, reason, server.ToJson(request))
194
195
196
197
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
Sietse Ringers's avatar
Sietse Ringers committed
198
	disjunctions := request.ToDisclose()
199
200
201
	if len(disjunctions) > 0 {
		allowed, reason := conf.CanVerifyOrSign(requestor, request.Action(), disjunctions)
		if !allowed {
202
203
			conf.Logger.Warn("Requestor %s tried to verify attribute %s but it is not authorized to; full request: %s",
				requestor, reason, server.ToJson(request))
204
205
206
207
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
208
209
210
211
212
	if rrequest.Base().CallbackUrl != "" && conf.jwtPrivateKey == nil {
		conf.Logger.Warn("Requestor %s provided callbackUrl but no JWT private key is installed")
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}
213
214

	// Everything is authenticated and parsed, we're good to go!
215
	qr, _, err := irmarequestor.StartSession(rrequest, doResultCallback)
216
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
217
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
218
219
220
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
221
	server.WriteJson(w, qr)
222
223
224
225
226
}

func handleStatus(w http.ResponseWriter, r *http.Request) {
	res := irmarequestor.GetSessionResult(chi.URLParam(r, "token"))
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
227
		server.WriteError(w, server.ErrorSessionUnknown, "")
228
229
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
230
	server.WriteJson(w, res.Status)
231
232
}

233
234
235
236
237
238
239
240
func handleStatusEvents(w http.ResponseWriter, r *http.Request) {
	token := chi.URLParam(r, "token")
	conf.Logger.Debug("new client subscribed to server sent events of session " + token)
	if err := irmarequestor.SubscribeServerSentEvents(w, r, token); err != nil {
		server.WriteError(w, server.ErrorUnexpectedRequest, err.Error())
	}
}

241
242
243
244
245
246
247
func handleDelete(w http.ResponseWriter, r *http.Request) {
	err := irmarequestor.CancelSession(chi.URLParam(r, "token"))
	if err != nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
	}
}

248
249
250
func handleResult(w http.ResponseWriter, r *http.Request) {
	res := irmarequestor.GetSessionResult(chi.URLParam(r, "token"))
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
251
		server.WriteError(w, server.ErrorSessionUnknown, "")
252
253
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
254
	server.WriteJson(w, res)
255
256
}

257
func handleJwtResult(w http.ResponseWriter, r *http.Request) {
258
	if conf.jwtPrivateKey == nil {
259
		conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
260
261
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
262
	}
263

264
265
	sessiontoken := chi.URLParam(r, "token")
	res := irmarequestor.GetSessionResult(sessiontoken)
266
267
268
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
269
	}
270

271
	j, err := resultJwt(res)
272
	if err != nil {
273
274
		conf.Logger.Error("Failed to sign session result JWT")
		_ = server.LogError(err)
275
276
277
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
278
	server.WriteString(w, j)
279
280
281
}

func handleJwtProofs(w http.ResponseWriter, r *http.Request) {
282
	if conf.jwtPrivateKey == nil {
283
		conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
284
285
286
287
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
	}

288
289
	sessiontoken := chi.URLParam(r, "token")
	res := irmarequestor.GetSessionResult(sessiontoken)
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
	}

	claims := jwt.MapClaims{}

	// Fill standard claims
	switch res.Type {
	case irma.ActionDisclosing:
		claims["subject"] = "verification_result"
	case irma.ActionSigning:
		claims["subject"] = "abs_result"
	default:
		if res == nil {
			server.WriteError(w, server.ErrorInvalidRequest, "")
			return
		}
	}
	claims["iat"] = time.Now().Unix()
	if conf.JwtIssuer != "" {
		claims["iss"] = conf.JwtIssuer
	}
	claims["status"] = res.Status
314
315
316
317
	validity := irmarequestor.GetRequest(sessiontoken).Base().ResultJwtValidity
	if validity != 0 {
		claims["exp"] = time.Now().Unix() + int64(validity)
	}
318
319
320
321
322
323
324
325
326
327
328
329
330

	// Disclosed credentials and possibly signature
	m := make(map[irma.AttributeTypeIdentifier]string, len(res.Disclosed))
	for _, attr := range res.Disclosed {
		m[attr.Identifier] = attr.Value[""]
	}
	claims["attributes"] = m
	if res.Signature != nil {
		claims["signature"] = res.Signature
	}

	// Sign the jwt and return it
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
331
	resultJwt, err := token.SignedString(conf.jwtPrivateKey)
332
	if err != nil {
333
334
		conf.Logger.Error("Failed to sign session result JWT")
		_ = server.LogError(err)
335
336
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
337
	}
338
	server.WriteString(w, resultJwt)
339
}
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357

func handlePublicKey(w http.ResponseWriter, r *http.Request) {
	if conf.jwtPrivateKey == nil {
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}

	bts, err := x509.MarshalPKIXPublicKey(&conf.jwtPrivateKey.PublicKey)
	if err != nil {
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
	pubBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: bts,
	})
	_, _ = w.Write(pubBytes)
}
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399

func resultJwt(sessionresult *server.SessionResult) (string, error) {
	claims := struct {
		jwt.StandardClaims
		*server.SessionResult
	}{
		StandardClaims: jwt.StandardClaims{
			Issuer:   conf.JwtIssuer,
			IssuedAt: time.Now().Unix(),
			Subject:  string(sessionresult.Type) + "_result",
		},
		SessionResult: sessionresult,
	}
	validity := irmarequestor.GetRequest(sessionresult.Token).Base().ResultJwtValidity
	if validity != 0 {
		claims.ExpiresAt = time.Now().Unix() + int64(validity)
	}

	// Sign the jwt and return it
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	return token.SignedString(conf.jwtPrivateKey)
}

func doResultCallback(result *server.SessionResult) {
	callbackUrl := irmarequestor.GetRequest(result.Token).Base().CallbackUrl
	if callbackUrl == "" || conf.jwtPrivateKey == nil {
		return
	}
	conf.Logger.Debug("POSTing session result to ", callbackUrl)

	j, err := resultJwt(result)
	if err != nil {
		_ = server.LogError(errors.WrapPrefix(err, "Failed to create JWT for result callback", 0))
		return
	}

	var x string // dummy for the server's return value that we don't care about
	if err := irma.NewHTTPTransport(callbackUrl).Post("", &x, j); err != nil {
		// not our problem, log it and go on
		conf.Logger.Warn(errors.WrapPrefix(err, "Failed to POST session result to callback URL", 0))
	}
}