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

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

Sietse Ringers's avatar
Sietse Ringers committed
29
// Server is a requestor server instance.
30
type Server struct {
31 32
	serv, clientserv *http.Server
	conf             *Configuration
33
	irmaserv         *irmaserver.Server
34
}
35

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

45
	// Start server(s)
46 47
	if s.conf.separateClientServer() {
		go s.startClientServer()
48
	}
49
	s.startRequestorServer()
50

51
	return nil
52 53
}

54 55 56 57
func (s *Server) startRequestorServer() {
	s.serv = &http.Server{}
	tlsConf, _ := s.conf.tlsConfig()
	s.startServer(s.serv, s.Handler(), "Server", s.conf.ListenAddress, s.conf.Port, tlsConf)
58 59
}

60 61 62 63
func (s *Server) startClientServer() {
	s.clientserv = &http.Server{}
	tlsConf, _ := s.conf.clientTlsConfig()
	s.startServer(s.clientserv, s.ClientHandler(), "Client server", s.conf.ClientListenAddress, s.conf.ClientPort, tlsConf)
64 65
}

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

86
func (s *Server) Stop() error {
87 88 89
	var err1, err2 error

	// Even if closing serv fails, we want to try closing clientserv
90 91 92
	err1 = s.serv.Close()
	if s.clientserv != nil {
		err2 = s.clientserv.Close()
93 94 95
	}

	// Now check errors
96
	if err1 != nil && err1 != http.ErrServerClosed {
97 98
		return err1
	}
99
	if err2 != nil && err2 != http.ErrServerClosed {
100 101 102 103 104
		return err2
	}
	return nil
}

105
func New(config *Configuration) (*Server, error) {
106
	irmaserv, err := irmaserver.New(config.Configuration)
107 108
	if err != nil {
		return nil, err
109
	}
110 111
	if err := config.initialize(); err != nil {
		return nil, err
112
	}
113 114 115 116
	return &Server{
		conf:     config,
		irmaserv: irmaserv,
	}, nil
117
}
118

119 120 121 122 123 124
var corsOptions = cors.Options{
	AllowedOrigins: []string{"*"},
	AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Cache-Control"},
	AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
}

125
func (s *Server) ClientHandler() http.Handler {
126
	router := chi.NewRouter()
127
	router.Use(cors.New(corsOptions).Handler)
Sietse Ringers's avatar
Sietse Ringers committed
128

Sietse Ringers's avatar
Sietse Ringers committed
129
	router.Mount("/irma/", s.irmaserv.HandlerFunc())
130
	if s.conf.StaticPath != "" {
131
		router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
132 133
	}

134 135 136 137 138
	return router
}

// Handler returns a http.Handler that handles all IRMA requestor messages
// and IRMA client messages.
139
func (s *Server) Handler() http.Handler {
140
	router := chi.NewRouter()
141
	router.Use(cors.New(corsOptions).Handler)
142

143
	if !s.conf.separateClientServer() {
144
		// Mount server for irmaclient
Sietse Ringers's avatar
Sietse Ringers committed
145
		router.Mount("/irma/", s.irmaserv.HandlerFunc())
146 147 148
		if s.conf.StaticPath != "" {
			router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
		}
149
	}
150

151
	// Server routes
152 153 154 155 156
	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
157 158

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

162
	router.Get("/publickey", s.handlePublicKey)
163

164
	return router
165 166
}

167 168 169 170 171 172 173 174 175 176 177
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))))
}

178
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
179 180
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
181
		s.conf.Logger.Error("Could not read session request HTTP POST body")
182
		_ = server.LogError(err)
Sietse Ringers's avatar
Sietse Ringers committed
183
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
184 185
		return
	}
186

Sietse Ringers's avatar
Sietse Ringers committed
187 188 189
	// 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.
190
	var (
191
		rrequest  irma.RequestorRequest
192 193 194 195 196
		request   irma.SessionRequest
		requestor string
		rerr      *irma.RemoteError
		applies   bool
	)
Sietse Ringers's avatar
Sietse Ringers committed
197
	for _, authenticator := range authenticators { // rrequest abbreviates "requestor request"
198
		applies, rrequest, requestor, rerr = authenticator.Authenticate(r.Header, body)
199 200 201 202 203
		if applies || rerr != nil {
			break
		}
	}
	if rerr != nil {
204
		_ = server.LogError(rerr)
205 206 207 208
		server.WriteResponse(w, nil, rerr)
		return
	}
	if !applies {
209
		s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s",
210
			server.ToJson(r.Header), string(body))
Sietse Ringers's avatar
Sietse Ringers committed
211
		server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
212 213 214
		return
	}

215 216
	// Authorize request: check if the requestor is allowed to verify or issue
	// the requested attributes or credentials
217
	request = rrequest.SessionRequest()
218
	if request.Action() == irma.ActionIssuing {
219
		allowed, reason := s.conf.CanIssue(requestor, request.(*irma.IssuanceRequest).Credentials)
220
		if !allowed {
221
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
222
				Warn("Requestor not authorized to issue credential; full request: ", server.ToJson(request))
223 224 225 226
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
Sietse Ringers's avatar
Sietse Ringers committed
227
	disjunctions := request.ToDisclose()
228
	if len(disjunctions) > 0 {
229
		allowed, reason := s.conf.CanVerifyOrSign(requestor, request.Action(), disjunctions)
230
		if !allowed {
231
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
232
				Warn("Requestor not authorized to verify attribute; full request: ", server.ToJson(request))
233 234 235 236
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
237 238
	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")
239 240 241
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}
242 243

	// Everything is authenticated and parsed, we're good to go!
244
	qr, _, err := s.irmaserv.StartSession(rrequest, s.doResultCallback)
245
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
246
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
247 248 249
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
250
	server.WriteJson(w, qr)
251 252
}

253 254
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
255
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
256
		server.WriteError(w, server.ErrorSessionUnknown, "")
257 258
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
259
	server.WriteJson(w, res.Status)
260 261
}

262
func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) {
263
	token := chi.URLParam(r, "token")
264 265
	s.conf.Logger.WithFields(logrus.Fields{"session": token}).Debug("new client subscribed to server sent events")
	if err := s.irmaserv.SubscribeServerSentEvents(w, r, token); err != nil {
266 267 268 269
		server.WriteError(w, server.ErrorUnexpectedRequest, err.Error())
	}
}

270 271
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
	err := s.irmaserv.CancelSession(chi.URLParam(r, "token"))
272 273 274 275 276
	if err != nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
	}
}

277 278
func (s *Server) handleResult(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
279
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
280
		server.WriteError(w, server.ErrorSessionUnknown, "")
281 282
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
283
	server.WriteJson(w, res)
284 285
}

286 287 288
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")
289 290
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
291
	}
292

293
	sessiontoken := chi.URLParam(r, "token")
294
	res := s.irmaserv.GetSessionResult(sessiontoken)
295 296 297
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
298
	}
299

300
	j, err := s.resultJwt(res)
301
	if err != nil {
302
		s.conf.Logger.Error("Failed to sign session result JWT")
303
		_ = server.LogError(err)
304 305 306
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
307
	server.WriteString(w, j)
308 309
}

310 311 312
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")
313 314 315 316
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
	}

317
	sessiontoken := chi.URLParam(r, "token")
318
	res := s.irmaserv.GetSessionResult(sessiontoken)
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
	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()
339 340
	if s.conf.JwtIssuer != "" {
		claims["iss"] = s.conf.JwtIssuer
341 342
	}
	claims["status"] = res.Status
343
	validity := s.irmaserv.GetRequest(sessiontoken).Base().ResultJwtValidity
344 345 346
	if validity != 0 {
		claims["exp"] = time.Now().Unix() + int64(validity)
	}
347 348 349 350 351 352 353 354 355 356 357 358 359

	// 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)
360
	resultJwt, err := token.SignedString(s.conf.jwtPrivateKey)
361
	if err != nil {
362
		s.conf.Logger.Error("Failed to sign session result JWT")
363
		_ = server.LogError(err)
364 365
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
366
	}
367
	server.WriteString(w, resultJwt)
368
}
369

370 371
func (s *Server) handlePublicKey(w http.ResponseWriter, r *http.Request) {
	if s.conf.jwtPrivateKey == nil {
372 373 374 375
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}

376
	bts, err := x509.MarshalPKIXPublicKey(&s.conf.jwtPrivateKey.PublicKey)
377 378 379 380 381 382 383 384 385 386
	if err != nil {
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
	pubBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: bts,
	})
	_, _ = w.Write(pubBytes)
}
387

388
func (s *Server) resultJwt(sessionresult *server.SessionResult) (string, error) {
389 390 391 392 393
	claims := struct {
		jwt.StandardClaims
		*server.SessionResult
	}{
		StandardClaims: jwt.StandardClaims{
394
			Issuer:   s.conf.JwtIssuer,
395 396 397 398 399
			IssuedAt: time.Now().Unix(),
			Subject:  string(sessionresult.Type) + "_result",
		},
		SessionResult: sessionresult,
	}
400
	validity := s.irmaserv.GetRequest(sessionresult.Token).Base().ResultJwtValidity
401 402 403 404 405 406
	if validity != 0 {
		claims.ExpiresAt = time.Now().Unix() + int64(validity)
	}

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

410 411 412
func (s *Server) doResultCallback(result *server.SessionResult) {
	callbackUrl := s.irmaserv.GetRequest(result.Token).Base().CallbackUrl
	if callbackUrl == "" || s.conf.jwtPrivateKey == nil {
413 414
		return
	}
415
	s.conf.Logger.WithFields(logrus.Fields{"session": result.Token, "callbackUrl": callbackUrl}).Debug("POSTing session result")
416

417
	j, err := s.resultJwt(result)
418 419 420 421 422 423 424 425
	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
426
		s.conf.Logger.Warn(errors.WrapPrefix(err, "Failed to POST session result to callback URL", 0))
427 428
	}
}