server.go 14.1 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
// Package requestorserver is a server allowing IRMA verifiers, issuers or attribute-based signature
Sietse Ringers's avatar
Sietse Ringers committed
2
3
4
// 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
5
package requestorserver
6
7

import (
8
	"context"
Sietse Ringers's avatar
Sietse Ringers committed
9
	"crypto/tls"
10
	"crypto/x509"
Sietse Ringers's avatar
Sietse Ringers committed
11
	"encoding/json"
12
	"encoding/pem"
13
	"fmt"
14
	"io/ioutil"
15
	"log"
16
	"net/http"
17
	"time"
18

19
	"github.com/dgrijalva/jwt-go"
20
	"github.com/go-chi/chi"
21
	"github.com/go-chi/chi/middleware"
Sietse Ringers's avatar
Sietse Ringers committed
22
	"github.com/go-chi/cors"
23
	"github.com/go-errors/errors"
24
	"github.com/privacybydesign/irmago"
Sietse Ringers's avatar
Sietse Ringers committed
25
	"github.com/privacybydesign/irmago/server"
26
	"github.com/privacybydesign/irmago/server/irmaserver"
27
	"github.com/sirupsen/logrus"
28
29
)

Sietse Ringers's avatar
Sietse Ringers committed
30
// Server is a requestor server instance.
31
type Server struct {
32
33
34
	conf     *Configuration
	irmaserv *irmaserver.Server
	stop     chan struct{}
35
}
36

37
// Start the server. If successful then it will not return until Stop() is called.
38
39
40
func (s *Server) Start(config *Configuration) error {
	if s.conf.LogJSON {
		s.conf.Logger.WithField("configuration", s.conf).Debug("Configuration")
41
	} else {
42
43
		bts, _ := json.MarshalIndent(s.conf, "", "   ")
		s.conf.Logger.Debug("Configuration: ", string(bts), "\n")
44
	}
45

46
47
48
49
50
51
52
53
54
	// We start either one or two servers, depending on whether a separate client server is enabled, such that:
	// - if any of them returns, the other is also stopped (neither of them is of use without the other)
	// - if any of them returns an unexpected error (ie. other than http.ErrServerClosed), the error is logged and returned
	// - we have a way of stopping all servers from outside (with Stop())
	// - the function returns only after all servers have been stopped
	// - any unexpected error is dealt with here instead of when stopping using Stop().
	// Inspired by https://dave.cheney.net/practical-go/presentations/qcon-china.html#_never_start_a_goroutine_without_when_it_will_stop

	count := 1
55
	if s.conf.separateClientServer() {
56
		count = 2
57
	}
58
59
	done := make(chan error, count)
	s.stop = make(chan struct{})
60

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
	if s.conf.separateClientServer() {
		go func() {
			done <- s.startClientServer()
		}()
	}
	go func() {
		done <- s.startRequestorServer()
	}()

	var stopped bool
	var err error
	for i := 0; i < cap(done); i++ {
		if err = <-done; err != nil {
			_ = server.LogError(err)
		}
		if !stopped {
			stopped = true
			close(s.stop)
		}
	}

	return err
83
84
}

85
func (s *Server) startRequestorServer() error {
86
	tlsConf, _ := s.conf.tlsConfig()
87
	return s.startServer(s.Handler(), "Server", s.conf.ListenAddress, s.conf.Port, tlsConf)
88
89
}

90
func (s *Server) startClientServer() error {
91
	tlsConf, _ := s.conf.clientTlsConfig()
92
	return s.startServer(s.ClientHandler(), "Client server", s.conf.ClientListenAddress, s.conf.ClientPort, tlsConf)
93
94
}

95
func (s *Server) startServer(handler http.Handler, name, addr string, port int, tlsConf *tls.Config) error {
96
	fulladdr := fmt.Sprintf("%s:%d", addr, port)
97
	s.conf.Logger.Info(name, " listening at ", fulladdr)
98
99
100
101
102
103
104
105
106
107
108
109
110
111

	serv := &http.Server{
		Addr:      fulladdr,
		Handler:   handler,
		TLSConfig: tlsConf,
	}

	go func() {
		<-s.stop
		if err := serv.Shutdown(context.Background()); err != nil {
			_ = server.LogError(err)
		}
	}()

Sietse Ringers's avatar
Sietse Ringers committed
112
	if tlsConf != nil {
113
		// Disable HTTP/2 (see package documentation of http): it breaks server side events :(
114
115
		serv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
		s.conf.Logger.Info(name, " TLS enabled")
116
		return filterStopError(serv.ListenAndServeTLS("", ""))
Sietse Ringers's avatar
Sietse Ringers committed
117
	} else {
118
		return filterStopError(serv.ListenAndServe())
119
120
121
	}
}

122
123
124
func filterStopError(err error) error {
	if err == http.ErrServerClosed {
		return nil
125
	}
126
127
	return err
}
128

129
130
131
func (s *Server) Stop() {
	s.irmaserv.Stop()
	s.stop <- struct{}{}
132
133
}

134
func New(config *Configuration) (*Server, error) {
135
	irmaserv, err := irmaserver.New(config.Configuration)
136
137
	if err != nil {
		return nil, err
138
	}
139
140
	if err := config.initialize(); err != nil {
		return nil, err
141
	}
142
143
144
145
	return &Server{
		conf:     config,
		irmaserv: irmaserv,
	}, nil
146
}
147

148
149
150
151
152
153
var corsOptions = cors.Options{
	AllowedOrigins: []string{"*"},
	AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Cache-Control"},
	AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
}

154
func (s *Server) ClientHandler() http.Handler {
155
	router := chi.NewRouter()
156
	router.Use(cors.New(corsOptions).Handler)
Sietse Ringers's avatar
Sietse Ringers committed
157

Sietse Ringers's avatar
Sietse Ringers committed
158
	router.Mount("/irma/", s.irmaserv.HandlerFunc())
159
	if s.conf.StaticPath != "" {
160
		router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
161
162
	}

163
164
165
166
167
	return router
}

// Handler returns a http.Handler that handles all IRMA requestor messages
// and IRMA client messages.
168
func (s *Server) Handler() http.Handler {
169
	router := chi.NewRouter()
170
	router.Use(cors.New(corsOptions).Handler)
171

172
	if !s.conf.separateClientServer() {
173
		// Mount server for irmaclient
Sietse Ringers's avatar
Sietse Ringers committed
174
		router.Mount("/irma/", s.irmaserv.HandlerFunc())
175
176
177
		if s.conf.StaticPath != "" {
			router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
		}
178
	}
179

180
	// Server routes
181
182
183
184
185
	router.Post("/session", s.handleCreate)
	router.Delete("/session/{token}", s.handleDelete)
	router.Get("/session/{token}/status", s.handleStatus)
	router.Get("/session/{token}/statusevents", s.handleStatusEvents)
	router.Get("/session/{token}/result", s.handleResult)
Sietse Ringers's avatar
Sietse Ringers committed
186
187

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

191
	router.Get("/publickey", s.handlePublicKey)
192

193
	return router
194
195
}

196
197
198
199
200
201
202
203
204
205
206
func (s *Server) StaticFilesHandler() http.Handler {
	url := s.conf.URL[:len(s.conf.URL)-6] + s.conf.StaticPrefix
	s.conf.Logger.Infof("Hosting files at %s under %s", s.conf.StaticPath, url)
	// Hook up chi middleware logger with our own logger
	middleware.DefaultLogger = middleware.RequestLogger(&middleware.DefaultLogFormatter{
		Logger:  log.New(s.conf.Logger.WriterLevel(logrus.TraceLevel), "static: ", 0),
		NoColor: true,
	})
	return http.StripPrefix(s.conf.StaticPrefix, middleware.Logger(http.FileServer(http.Dir(s.conf.StaticPath))))
}

207
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
208
209
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
210
		s.conf.Logger.Error("Could not read session request HTTP POST body")
211
		_ = server.LogError(err)
Sietse Ringers's avatar
Sietse Ringers committed
212
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
213
214
		return
	}
215

Sietse Ringers's avatar
Sietse Ringers committed
216
217
218
	// 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.
219
	var (
220
		rrequest  irma.RequestorRequest
221
222
223
224
225
		request   irma.SessionRequest
		requestor string
		rerr      *irma.RemoteError
		applies   bool
	)
Sietse Ringers's avatar
Sietse Ringers committed
226
	for _, authenticator := range authenticators { // rrequest abbreviates "requestor request"
227
		applies, rrequest, requestor, rerr = authenticator.Authenticate(r.Header, body)
228
229
230
231
232
		if applies || rerr != nil {
			break
		}
	}
	if rerr != nil {
233
		_ = server.LogError(rerr)
234
235
236
237
		server.WriteResponse(w, nil, rerr)
		return
	}
	if !applies {
238
		s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s",
239
			server.ToJson(r.Header), string(body))
Sietse Ringers's avatar
Sietse Ringers committed
240
		server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
241
242
243
		return
	}

244
245
	// Authorize request: check if the requestor is allowed to verify or issue
	// the requested attributes or credentials
246
	request = rrequest.SessionRequest()
247
	if request.Action() == irma.ActionIssuing {
248
		allowed, reason := s.conf.CanIssue(requestor, request.(*irma.IssuanceRequest).Credentials)
249
		if !allowed {
250
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
251
				Warn("Requestor not authorized to issue credential; full request: ", server.ToJson(request))
252
253
254
255
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
Sietse Ringers's avatar
Sietse Ringers committed
256
	disjunctions := request.ToDisclose()
257
	if len(disjunctions) > 0 {
258
		allowed, reason := s.conf.CanVerifyOrSign(requestor, request.Action(), disjunctions)
259
		if !allowed {
260
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
261
				Warn("Requestor not authorized to verify attribute; full request: ", server.ToJson(request))
262
263
264
265
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
266
267
	if rrequest.Base().CallbackUrl != "" && s.conf.jwtPrivateKey == nil {
		s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor}).Warn("Requestor provided callbackUrl but no JWT private key is installed")
268
269
270
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}
271
272

	// Everything is authenticated and parsed, we're good to go!
273
	qr, token, err := s.irmaserv.StartSession(rrequest, s.doResultCallback)
274
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
275
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
276
277
278
		return
	}

279
280
281
282
	server.WriteJson(w, server.SessionPackage{
		SessionPtr: qr,
		Token:      token,
	})
283
284
}

285
286
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
287
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
288
		server.WriteError(w, server.ErrorSessionUnknown, "")
289
290
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
291
	server.WriteJson(w, res.Status)
292
293
}

294
func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) {
295
	token := chi.URLParam(r, "token")
296
	s.conf.Logger.WithFields(logrus.Fields{"session": token}).Debug("new client subscribed to server sent events")
297
	if err := s.irmaserv.SubscribeServerSentEvents(w, r, token, true); err != nil {
298
299
300
301
		server.WriteError(w, server.ErrorUnexpectedRequest, err.Error())
	}
}

302
303
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
	err := s.irmaserv.CancelSession(chi.URLParam(r, "token"))
304
305
306
307
308
	if err != nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
	}
}

309
310
func (s *Server) handleResult(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
311
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
312
		server.WriteError(w, server.ErrorSessionUnknown, "")
313
314
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
315
	server.WriteJson(w, res)
316
317
}

318
319
320
func (s *Server) handleJwtResult(w http.ResponseWriter, r *http.Request) {
	if s.conf.jwtPrivateKey == nil {
		s.conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
321
322
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
323
	}
324

325
	sessiontoken := chi.URLParam(r, "token")
326
	res := s.irmaserv.GetSessionResult(sessiontoken)
327
328
329
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
330
	}
331

332
	j, err := s.resultJwt(res)
333
	if err != nil {
334
		s.conf.Logger.Error("Failed to sign session result JWT")
335
		_ = server.LogError(err)
336
337
338
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
339
	server.WriteString(w, j)
340
341
}

342
343
344
func (s *Server) handleJwtProofs(w http.ResponseWriter, r *http.Request) {
	if s.conf.jwtPrivateKey == nil {
		s.conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
345
346
347
348
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
	}

349
	sessiontoken := chi.URLParam(r, "token")
350
	res := s.irmaserv.GetSessionResult(sessiontoken)
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
	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()
371
372
	if s.conf.JwtIssuer != "" {
		claims["iss"] = s.conf.JwtIssuer
373
374
	}
	claims["status"] = res.Status
375
	validity := s.irmaserv.GetRequest(sessiontoken).Base().ResultJwtValidity
376
377
378
	if validity != 0 {
		claims["exp"] = time.Now().Unix() + int64(validity)
	}
379
380
381
382
383
384
385
386
387
388
389
390
391

	// 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)
392
	resultJwt, err := token.SignedString(s.conf.jwtPrivateKey)
393
	if err != nil {
394
		s.conf.Logger.Error("Failed to sign session result JWT")
395
		_ = server.LogError(err)
396
397
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
398
	}
399
	server.WriteString(w, resultJwt)
400
}
401

402
403
func (s *Server) handlePublicKey(w http.ResponseWriter, r *http.Request) {
	if s.conf.jwtPrivateKey == nil {
404
405
406
407
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}

408
	bts, err := x509.MarshalPKIXPublicKey(&s.conf.jwtPrivateKey.PublicKey)
409
410
411
412
413
414
415
416
417
418
	if err != nil {
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
	pubBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: bts,
	})
	_, _ = w.Write(pubBytes)
}
419

420
func (s *Server) resultJwt(sessionresult *server.SessionResult) (string, error) {
421
422
423
424
425
	claims := struct {
		jwt.StandardClaims
		*server.SessionResult
	}{
		StandardClaims: jwt.StandardClaims{
426
			Issuer:   s.conf.JwtIssuer,
427
428
429
430
431
			IssuedAt: time.Now().Unix(),
			Subject:  string(sessionresult.Type) + "_result",
		},
		SessionResult: sessionresult,
	}
432
	validity := s.irmaserv.GetRequest(sessionresult.Token).Base().ResultJwtValidity
433
434
435
436
437
438
	if validity != 0 {
		claims.ExpiresAt = time.Now().Unix() + int64(validity)
	}

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

442
443
444
func (s *Server) doResultCallback(result *server.SessionResult) {
	callbackUrl := s.irmaserv.GetRequest(result.Token).Base().CallbackUrl
	if callbackUrl == "" || s.conf.jwtPrivateKey == nil {
445
446
		return
	}
447
	s.conf.Logger.WithFields(logrus.Fields{"session": result.Token, "callbackUrl": callbackUrl}).Debug("POSTing session result")
448

449
	j, err := s.resultJwt(result)
450
451
452
453
454
455
456
457
	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
458
		s.conf.Logger.Warn(errors.WrapPrefix(err, "Failed to POST session result to callback URL", 0))
459
460
	}
}