server.go 15.8 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
	"bytes"
9
	"context"
Sietse Ringers's avatar
Sietse Ringers committed
10
	"crypto/tls"
11
	"crypto/x509"
Sietse Ringers's avatar
Sietse Ringers committed
12
	"encoding/json"
13
	"encoding/pem"
14
	"fmt"
15
	"io/ioutil"
16
	"log"
17
	"net/http"
18
	"strings"
19
	"time"
20

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

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

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

49 50 51 52 53 54 55 56 57
	// 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
58
	if s.conf.separateClientServer() {
59
		count = 2
60
	}
61 62
	done := make(chan error, count)
	s.stop = make(chan struct{})
63
	s.stopped = make(chan struct{}, count)
64

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
	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
87 88
}

89
func (s *Server) startRequestorServer() error {
90
	tlsConf, _ := s.conf.tlsConfig()
91
	return s.startServer(s.Handler(), "Server", s.conf.ListenAddress, s.conf.Port, tlsConf)
92 93
}

94
func (s *Server) startClientServer() error {
95
	tlsConf, _ := s.conf.clientTlsConfig()
96
	return s.startServer(s.ClientHandler(), "Client server", s.conf.ClientListenAddress, s.conf.ClientPort, tlsConf)
97 98
}

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

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

	go func() {
		<-s.stop
111 112 113
		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
		defer cancel()
		if err := serv.Shutdown(ctx); err != nil {
114 115
			_ = server.LogError(err)
		}
116
		s.stopped <- struct{}{}
117 118
	}()

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

129 130 131
func filterStopError(err error) error {
	if err == http.ErrServerClosed {
		return nil
132
	}
133 134
	return err
}
135

136 137 138
func (s *Server) Stop() {
	s.irmaserv.Stop()
	s.stop <- struct{}{}
139 140 141 142
	<-s.stopped
	if s.conf.separateClientServer() {
		<-s.stopped
	}
143 144
}

145
func New(config *Configuration) (*Server, error) {
146
	irmaserv, err := irmaserver.New(config.Configuration)
147 148
	if err != nil {
		return nil, err
149
	}
150 151
	if err := config.initialize(); err != nil {
		return nil, err
152
	}
153 154 155 156
	return &Server{
		conf:     config,
		irmaserv: irmaserv,
	}, nil
157
}
158

159 160 161 162 163 164
var corsOptions = cors.Options{
	AllowedOrigins: []string{"*"},
	AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Cache-Control"},
	AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
}

165
func (s *Server) ClientHandler() http.Handler {
166
	router := chi.NewRouter()
167
	router.Use(cors.New(corsOptions).Handler)
Sietse Ringers's avatar
Sietse Ringers committed
168

Sietse Ringers's avatar
Sietse Ringers committed
169
	router.Mount("/irma/", s.irmaserv.HandlerFunc())
170
	if s.conf.StaticPath != "" {
171
		router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
172 173
	}

174 175 176 177 178
	return router
}

// Handler returns a http.Handler that handles all IRMA requestor messages
// and IRMA client messages.
179
func (s *Server) Handler() http.Handler {
180
	router := chi.NewRouter()
181
	router.Use(cors.New(corsOptions).Handler)
182
	router.Use(s.logHandler)
183

184
	if !s.conf.separateClientServer() {
185
		// Mount server for irmaclient
Sietse Ringers's avatar
Sietse Ringers committed
186
		router.Mount("/irma/", s.irmaserv.HandlerFunc())
187 188 189
		if s.conf.StaticPath != "" {
			router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
		}
190
	}
191

192
	// Server routes
193 194 195 196 197
	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
198 199

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

203
	router.Get("/publickey", s.handlePublicKey)
204

205
	return router
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
// Wrapper for logging HTTP traffic
func (s *Server) logHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
		if s.conf.Verbose >= 2 {
			t1 := time.Now()
			respBuf := bytes.NewBufferString(": ") // : for printing format
			ww.Tee(respBuf)                        // Copy result into buffer for logging
			scheme := "http"
			if r.TLS != nil {
				scheme = "https"
			}

			recipient := ""
			// IP addresses of irmaclient users are hidden in logs for privacy
			if !strings.HasPrefix(r.URL.EscapedPath(), "/irma/") {
				recipient = " from " + r.RemoteAddr
			}
			s.conf.Logger.Tracef("=> New message \"%s %s://%s%s %s\"%s",
				r.Method, scheme, r.Host, r.RequestURI, r.Proto, recipient)
			s.conf.Logger.Trace("HTTP headers: ", server.ToJson(r.Header))

			defer func() {
				// Printing response after HTTP is served
				resp := ""
				if ww.BytesWritten() > 0 {
					resp = respBuf.String()
				}
				s.conf.Logger.Tracef("<= Responded with %d in %s%s", ww.Status(), time.Since(t1).String(), resp)
			}()
		}

		next.ServeHTTP(ww, r)
		return
	})
}

245
func (s *Server) StaticFilesHandler() http.Handler {
246 247 248 249 250 251
	if len(s.conf.URL) > 6 {
		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)
	} else { // URL not known, don't log it but otherwise continue
		s.conf.Logger.Infof("Hosting files at %s", s.conf.StaticPath)
	}
252 253 254 255 256 257 258 259
	// 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))))
}

260
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
261 262
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
263
		s.conf.Logger.Error("Could not read session request HTTP POST body")
264
		_ = server.LogError(err)
Sietse Ringers's avatar
Sietse Ringers committed
265
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
266 267
		return
	}
268

Sietse Ringers's avatar
Sietse Ringers committed
269 270 271
	// 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.
272
	var (
273
		rrequest  irma.RequestorRequest
274 275 276 277 278
		request   irma.SessionRequest
		requestor string
		rerr      *irma.RemoteError
		applies   bool
	)
Sietse Ringers's avatar
Sietse Ringers committed
279
	for _, authenticator := range authenticators { // rrequest abbreviates "requestor request"
280
		applies, rrequest, requestor, rerr = authenticator.Authenticate(r.Header, body)
281 282 283 284 285
		if applies || rerr != nil {
			break
		}
	}
	if rerr != nil {
286
		_ = server.LogError(rerr)
287 288 289 290
		server.WriteResponse(w, nil, rerr)
		return
	}
	if !applies {
291
		s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s",
292
			server.ToJson(r.Header), string(body))
Sietse Ringers's avatar
Sietse Ringers committed
293
		server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
294 295 296
		return
	}

297 298
	// Authorize request: check if the requestor is allowed to verify or issue
	// the requested attributes or credentials
299
	request = rrequest.SessionRequest()
300
	if request.Action() == irma.ActionIssuing {
301
		allowed, reason := s.conf.CanIssue(requestor, request.(*irma.IssuanceRequest).Credentials)
302
		if !allowed {
303
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
304
				Warn("Requestor not authorized to issue credential; full request: ", server.ToJson(request))
305 306 307 308
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
309 310 311
	condiscon := request.Disclosure().Disclose
	if len(condiscon) > 0 {
		allowed, reason := s.conf.CanVerifyOrSign(requestor, request.Action(), condiscon)
312
		if !allowed {
313
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
314
				Warn("Requestor not authorized to verify attribute; full request: ", server.ToJson(request))
315 316 317 318
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
319 320
	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")
321 322 323
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}
324 325

	// Everything is authenticated and parsed, we're good to go!
326
	qr, token, err := s.irmaserv.StartSession(rrequest, s.doResultCallback)
327
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
328
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
329 330 331
		return
	}

332 333 334 335
	server.WriteJson(w, server.SessionPackage{
		SessionPtr: qr,
		Token:      token,
	})
336 337
}

338 339
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
340
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
341
		server.WriteError(w, server.ErrorSessionUnknown, "")
342 343
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
344
	server.WriteJson(w, res.Status)
345 346
}

347
func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) {
348
	token := chi.URLParam(r, "token")
349
	s.conf.Logger.WithFields(logrus.Fields{"session": token}).Debug("new client subscribed to server sent events")
350
	if err := s.irmaserv.SubscribeServerSentEvents(w, r, token, true); err != nil {
351 352 353 354 355
		server.WriteResponse(w, nil, &irma.RemoteError{
			Status:      server.ErrorUnsupported.Status,
			ErrorName:   string(server.ErrorUnsupported.Type),
			Description: server.ErrorUnsupported.Description,
		})
356 357 358
	}
}

359 360
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
	err := s.irmaserv.CancelSession(chi.URLParam(r, "token"))
361 362 363 364 365
	if err != nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
	}
}

366 367
func (s *Server) handleResult(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
368
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
369
		server.WriteError(w, server.ErrorSessionUnknown, "")
370 371
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
372
	server.WriteJson(w, res)
373 374
}

375 376 377
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")
378 379
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
380
	}
381

382
	sessiontoken := chi.URLParam(r, "token")
383
	res := s.irmaserv.GetSessionResult(sessiontoken)
384 385 386
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
387
	}
388

389
	j, err := s.resultJwt(res)
390
	if err != nil {
391
		s.conf.Logger.Error("Failed to sign session result JWT")
392
		_ = server.LogError(err)
393 394 395
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
396
	server.WriteString(w, j)
397 398
}

399 400 401
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")
402 403 404 405
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
	}

406
	sessiontoken := chi.URLParam(r, "token")
407
	res := s.irmaserv.GetSessionResult(sessiontoken)
408 409 410 411 412 413 414 415 416 417
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
	}

	claims := jwt.MapClaims{}

	// Fill standard claims
	switch res.Type {
	case irma.ActionDisclosing:
418
		claims["sub"] = "disclosure_result"
419
	case irma.ActionSigning:
420
		claims["sub"] = "abs_result"
421
	default:
422 423
		server.WriteError(w, server.ErrorInvalidRequest, "")
		return
424 425
	}
	claims["iat"] = time.Now().Unix()
426 427
	if s.conf.JwtIssuer != "" {
		claims["iss"] = s.conf.JwtIssuer
428
	}
429
	claims["status"] = res.ProofStatus
430
	validity := s.irmaserv.GetRequest(sessiontoken).Base().ResultJwtValidity
431 432 433
	if validity != 0 {
		claims["exp"] = time.Now().Unix() + int64(validity)
	}
434 435 436

	// Disclosed credentials and possibly signature
	m := make(map[irma.AttributeTypeIdentifier]string, len(res.Disclosed))
437 438 439 440
	for _, set := range res.Disclosed {
		for _, attr := range set {
			m[attr.Identifier] = attr.Value[""]
		}
441 442 443 444 445 446 447 448
	}
	claims["attributes"] = m
	if res.Signature != nil {
		claims["signature"] = res.Signature
	}

	// Sign the jwt and return it
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
449
	resultJwt, err := token.SignedString(s.conf.jwtPrivateKey)
450
	if err != nil {
451
		s.conf.Logger.Error("Failed to sign session result JWT")
452
		_ = server.LogError(err)
453 454
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
455
	}
456
	server.WriteString(w, resultJwt)
457
}
458

459 460
func (s *Server) handlePublicKey(w http.ResponseWriter, r *http.Request) {
	if s.conf.jwtPrivateKey == nil {
461 462 463 464
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}

465
	bts, err := x509.MarshalPKIXPublicKey(&s.conf.jwtPrivateKey.PublicKey)
466 467 468 469 470 471 472 473 474 475
	if err != nil {
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
	pubBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: bts,
	})
	_, _ = w.Write(pubBytes)
}
476

477
func (s *Server) resultJwt(sessionresult *server.SessionResult) (string, error) {
478 479 480 481 482
	claims := struct {
		jwt.StandardClaims
		*server.SessionResult
	}{
		StandardClaims: jwt.StandardClaims{
483
			Issuer:   s.conf.JwtIssuer,
484 485 486 487 488
			IssuedAt: time.Now().Unix(),
			Subject:  string(sessionresult.Type) + "_result",
		},
		SessionResult: sessionresult,
	}
489
	validity := s.irmaserv.GetRequest(sessionresult.Token).Base().ResultJwtValidity
490 491 492 493 494 495
	if validity != 0 {
		claims.ExpiresAt = time.Now().Unix() + int64(validity)
	}

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

499 500 501
func (s *Server) doResultCallback(result *server.SessionResult) {
	callbackUrl := s.irmaserv.GetRequest(result.Token).Base().CallbackUrl
	if callbackUrl == "" || s.conf.jwtPrivateKey == nil {
502 503
		return
	}
504
	s.conf.Logger.WithFields(logrus.Fields{"session": result.Token, "callbackUrl": callbackUrl}).Debug("POSTing session result")
505

506
	j, err := s.resultJwt(result)
507 508 509 510 511 512 513 514
	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
515
		s.conf.Logger.Warn(errors.WrapPrefix(err, "Failed to POST session result to callback URL", 0))
516 517
	}
}