server.go 18.9 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 16
	"io/ioutil"
	"net/http"
17
	"strings"
18
	"time"
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

171
func (s *Server) attachClientEndpoints(router *chi.Mux) {
Sietse Ringers's avatar
Sietse Ringers committed
172
	router.Mount("/irma/", s.irmaserv.HandlerFunc())
173
	if s.conf.StaticPath != "" {
174
		router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
175
	}
176 177 178 179
	router.Group(func(r chi.Router) {
		if s.conf.Verbose >= 2 {
			r.Use(s.logHandler("staticsession", true, true, true))
		}
180
		r.Post("/irma/session/{name}", s.handleCreateStatic)
181
	})
182 183 184 185
}

// Handler returns a http.Handler that handles all IRMA requestor messages
// and IRMA client messages.
186
func (s *Server) Handler() http.Handler {
187
	router := chi.NewRouter()
188
	router.Use(cors.New(corsOptions).Handler)
189

190
	if !s.conf.separateClientServer() {
191
		// Mount server for irmaclient
192
		s.attachClientEndpoints(router)
193
	}
194

195 196 197
	router.NotFound(s.logHandler("requestor", false, true, true)(router.NotFoundHandler()).ServeHTTP)
	router.MethodNotAllowed(s.logHandler("requestor", false, true, true)(router.MethodNotAllowedHandler()).ServeHTTP)

198 199 200 201 202
	// Group main API endpoints, so we can attach our request/response logger to it
	// while not adding it to the endpoints already added above (which do their own logging).
	router.Group(func(r chi.Router) {
		r.Use(cors.New(corsOptions).Handler)
		if s.conf.Verbose >= 2 {
203
			r.Use(s.logHandler("requestor", true, true, true))
204 205 206 207 208 209 210 211
		}

		// Server routes
		r.Post("/session", s.handleCreate)
		r.Delete("/session/{token}", s.handleDelete)
		r.Get("/session/{token}/status", s.handleStatus)
		r.Get("/session/{token}/statusevents", s.handleStatusEvents)
		r.Get("/session/{token}/result", s.handleResult)
Sietse Ringers's avatar
Sietse Ringers committed
212

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

217 218
		r.Get("/publickey", s.handlePublicKey)
	})
219

220
	return router
221 222
}

223
// logHandler is middleware for logging HTTP requests and responses.
224 225 226 227 228 229 230 231 232 233 234 235
func (s *Server) logHandler(typ string, logResponse, logHeaders, logFrom bool) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			var message []byte
			var err error

			// Read r.Body, and then replace with a fresh ReadCloser for the next handler
			if message, err = ioutil.ReadAll(r.Body); err != nil {
				message = []byte("<failed to read body: " + err.Error() + ">")
			}
			_ = r.Body.Close()
			r.Body = ioutil.NopCloser(bytes.NewBuffer(message))
236

237 238 239 240 241 242 243 244
			var headers http.Header
			var from string
			if logHeaders {
				headers = r.Header
			}
			if logFrom {
				from = r.RemoteAddr
			}
245
			server.LogRequest(typ, r.Method, r.URL.String(), from, headers, message)
246 247 248 249 250 251 252

			// copy output of HTTP handler to our buffer for later logging
			ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
			var buf *bytes.Buffer
			if logResponse {
				buf = new(bytes.Buffer)
				ww.Tee(buf)
253 254
			}

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
			// print response afterwards
			var resp []byte
			var start time.Time
			defer func() {
				if logResponse && ww.BytesWritten() > 0 {
					resp = buf.Bytes()
				}
				server.LogResponse(ww.Status(), time.Since(start), resp)
			}()

			// start timer and preform request
			start = time.Now()
			next.ServeHTTP(ww, r)
		})
	}
270 271
}

272
func (s *Server) StaticFilesHandler() http.Handler {
273 274 275 276 277 278
	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)
	}
279 280 281
	return http.StripPrefix(s.conf.StaticPrefix, s.logHandler("static", false, false, false)(
		http.FileServer(http.Dir(s.conf.StaticPath))),
	)
282 283
}

284
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
285 286
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
287
		s.conf.Logger.Error("Could not read session request HTTP POST body")
288
		_ = server.LogError(err)
Sietse Ringers's avatar
Sietse Ringers committed
289
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
290 291
		return
	}
292

Sietse Ringers's avatar
Sietse Ringers committed
293 294 295
	// 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.
296
	var (
297
		rrequest  irma.RequestorRequest
298
		revreq    *irma.RevocationRequest
299 300 301 302
		requestor string
		rerr      *irma.RemoteError
		applies   bool
	)
Sietse Ringers's avatar
Sietse Ringers committed
303
	for _, authenticator := range authenticators { // rrequest abbreviates "requestor request"
304
		applies, rrequest, requestor, rerr = authenticator.AuthenticateSession(r.Header, body)
305 306 307
		if applies || rerr != nil {
			break
		}
308 309 310 311
		applies, revreq, requestor, rerr = authenticator.AuthenticateRevocation(r.Header, body)
		if applies || rerr != nil {
			break
		}
312 313
	}
	if rerr != nil {
314
		_ = server.LogError(rerr)
315 316 317 318
		server.WriteResponse(w, nil, rerr)
		return
	}
	if !applies {
Leon's avatar
Leon committed
319 320 321 322
		var ctype = r.Header.Get("Content-Type")
		if ctype != "application/json" && ctype != "text/plain" {
			s.conf.Logger.Warnf("Session request uses unsupported Content-Type: %s", ctype)
			server.WriteError(w, server.ErrorInvalidRequest, "Unsupported Content-Type: "+ctype)
323 324 325 326
			return
		}
		s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s", server.ToJson(r.Header), string(body))
		server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authenticated")
327 328 329
		return
	}

330 331 332
	if rrequest != nil {
		s.handleCreateSession(w, requestor, rrequest)
	} else {
333
		s.handleRevoke(w, requestor, revreq)
334 335 336 337
	}
}

func (s *Server) handleCreateSession(w http.ResponseWriter, requestor string, rrequest irma.RequestorRequest) {
338 339
	// Authorize request: check if the requestor is allowed to verify or issue
	// the requested attributes or credentials
340
	request := rrequest.SessionRequest()
341
	if request.Action() == irma.ActionIssuing {
342
		allowed, reason := s.conf.CanIssue(requestor, request.(*irma.IssuanceRequest).Credentials)
343
		if !allowed {
344
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
345
				Warn("Requestor not authorized to issue credential; full request: ", server.ToJson(request))
346 347 348 349
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
350 351 352
	condiscon := request.Disclosure().Disclose
	if len(condiscon) > 0 {
		allowed, reason := s.conf.CanVerifyOrSign(requestor, request.Action(), condiscon)
353
		if !allowed {
354
			s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "id": reason}).
355
				Warn("Requestor not authorized to verify attribute; full request: ", server.ToJson(request))
356 357 358 359
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
360
	if rrequest.Base().CallbackURL != "" && s.conf.jwtPrivateKey == nil {
361
		s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor}).Warn("Requestor provided callbackUrl but no JWT private key is installed")
362 363 364
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}
365 366

	// Everything is authenticated and parsed, we're good to go!
367
	qr, token, err := s.irmaserv.StartSession(rrequest, s.doResultCallback)
368
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
369
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
370 371 372
		return
	}

373 374 375 376
	server.WriteJson(w, server.SessionPackage{
		SessionPtr: qr,
		Token:      token,
	})
377 378
}

379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
func (s *Server) handleCreateStatic(w http.ResponseWriter, r *http.Request) {
	name := chi.URLParam(r, "name")
	rrequest := s.conf.staticSessions[name]
	if rrequest == nil {
		server.WriteError(w, server.ErrorInvalidRequest, "unknown static session")
		return
	}
	qr, _, err := s.irmaserv.StartSession(rrequest, s.doResultCallback)
	if err != nil {
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
		return
	}
	server.WriteJson(w, qr)
}

394
func (s *Server) handleRevoke(w http.ResponseWriter, requestor string, request *irma.RevocationRequest) {
395 396 397 398 399 400 401
	allowed, reason := s.conf.CanRevoke(requestor, request.CredentialType)
	if !allowed {
		s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "message": reason}).
			Warn("Requestor not authorized to revoke credential; full request: ", server.ToJson(request))
		server.WriteError(w, server.ErrorUnauthorized, reason)
		return
	}
402
	if err := s.irmaserv.Revoke(request.CredentialType, request.Key); err != nil {
403 404 405 406 407
		server.WriteError(w, server.ErrorUnknown, err.Error())
	}
	server.WriteString(w, "OK")
}

408 409
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
410
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
411
		server.WriteError(w, server.ErrorSessionUnknown, "")
412 413
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
414
	server.WriteJson(w, res.Status)
415 416
}

417
func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) {
418
	token := chi.URLParam(r, "token")
419
	s.conf.Logger.WithFields(logrus.Fields{"session": token}).Debug("new client subscribed to server sent events")
420
	if err := s.irmaserv.SubscribeServerSentEvents(w, r, token, true); err != nil {
421 422 423 424 425
		server.WriteResponse(w, nil, &irma.RemoteError{
			Status:      server.ErrorUnsupported.Status,
			ErrorName:   string(server.ErrorUnsupported.Type),
			Description: server.ErrorUnsupported.Description,
		})
426 427 428
	}
}

429 430
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
	err := s.irmaserv.CancelSession(chi.URLParam(r, "token"))
431 432 433 434 435
	if err != nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
	}
}

436 437
func (s *Server) handleResult(w http.ResponseWriter, r *http.Request) {
	res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
438
	if res == nil {
Sietse Ringers's avatar
Sietse Ringers committed
439
		server.WriteError(w, server.ErrorSessionUnknown, "")
440 441
		return
	}
442 443 444 445 446
	if res.LegacySession {
		server.WriteJson(w, res.Legacy())
	} else {
		server.WriteJson(w, res)
	}
447 448
}

449 450 451
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")
452 453
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
454
	}
455

456
	sessiontoken := chi.URLParam(r, "token")
457
	res := s.irmaserv.GetSessionResult(sessiontoken)
458 459 460
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
461
	}
462

463
	j, err := s.resultJwt(res)
464
	if err != nil {
465
		s.conf.Logger.Error("Failed to sign session result JWT")
466
		_ = server.LogError(err)
467 468 469
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
470
	server.WriteString(w, j)
471 472
}

473 474 475
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")
476 477 478 479
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
	}

480
	sessiontoken := chi.URLParam(r, "token")
481
	res := s.irmaserv.GetSessionResult(sessiontoken)
482 483 484 485 486 487 488 489 490 491
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
	}

	claims := jwt.MapClaims{}

	// Fill standard claims
	switch res.Type {
	case irma.ActionDisclosing:
492
		claims["sub"] = "disclosure_result"
493
	case irma.ActionSigning:
494
		claims["sub"] = "abs_result"
495 496
	case irma.ActionIssuing:
		claims["sub"] = "issue_result"
497
	default:
498 499
		server.WriteError(w, server.ErrorInvalidRequest, "")
		return
500 501
	}
	claims["iat"] = time.Now().Unix()
502 503
	if s.conf.JwtIssuer != "" {
		claims["iss"] = s.conf.JwtIssuer
504
	}
505
	claims["status"] = res.ProofStatus
506
	validity := s.irmaserv.GetRequest(sessiontoken).Base().ResultJwtValidity
507 508 509
	if validity != 0 {
		claims["exp"] = time.Now().Unix() + int64(validity)
	}
510 511 512

	// Disclosed credentials and possibly signature
	m := make(map[irma.AttributeTypeIdentifier]string, len(res.Disclosed))
513 514 515 516
	for _, set := range res.Disclosed {
		for _, attr := range set {
			m[attr.Identifier] = attr.Value[""]
		}
517 518 519 520 521 522 523 524
	}
	claims["attributes"] = m
	if res.Signature != nil {
		claims["signature"] = res.Signature
	}

	// Sign the jwt and return it
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
525
	resultJwt, err := token.SignedString(s.conf.jwtPrivateKey)
526
	if err != nil {
527
		s.conf.Logger.Error("Failed to sign session result JWT")
528
		_ = server.LogError(err)
529 530
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
531
	}
532
	server.WriteString(w, resultJwt)
533
}
534

535 536
func (s *Server) handlePublicKey(w http.ResponseWriter, r *http.Request) {
	if s.conf.jwtPrivateKey == nil {
537 538 539 540
		server.WriteError(w, server.ErrorUnsupported, "")
		return
	}

541
	bts, err := x509.MarshalPKIXPublicKey(&s.conf.jwtPrivateKey.PublicKey)
542 543 544 545 546 547 548 549 550 551
	if err != nil {
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
	pubBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: bts,
	})
	_, _ = w.Write(pubBytes)
}
552

553
func (s *Server) resultJwt(sessionresult *server.SessionResult) (string, error) {
554 555 556 557
	standardclaims := jwt.StandardClaims{
		Issuer:   s.conf.JwtIssuer,
		IssuedAt: time.Now().Unix(),
		Subject:  string(sessionresult.Type) + "_result",
558
	}
559
	validity := s.irmaserv.GetRequest(sessionresult.Token).Base().ResultJwtValidity
560 561 562 563 564 565 566 567 568 569 570 571 572
	standardclaims.ExpiresAt = time.Now().Unix() + int64(validity)

	var claims jwt.Claims
	if sessionresult.LegacySession {
		claims = struct {
			jwt.StandardClaims
			*server.LegacySessionResult
		}{standardclaims, sessionresult.Legacy()}
	} else {
		claims = struct {
			jwt.StandardClaims
			*server.SessionResult
		}{standardclaims, sessionresult}
573 574 575 576
	}

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

580
func (s *Server) doResultCallback(result *server.SessionResult) {
581
	callbackUrl := s.irmaserv.GetRequest(result.Token).Base().CallbackURL
582
	if callbackUrl == "" {
583 584 585
		return
	}

586 587
	logger := s.conf.Logger.WithFields(logrus.Fields{"session": result.Token, "callbackUrl": callbackUrl})
	if !strings.HasPrefix(callbackUrl, "https") {
588
		logger.Warn("POSTing session result to callback URL without TLS: attributes are unencrypted in traffic")
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
	} else {
		logger.Debug("POSTing session result")
	}

	var res string
	if s.conf.jwtPrivateKey != nil {
		var err error
		res, err = s.resultJwt(result)
		if err != nil {
			_ = server.LogError(errors.WrapPrefix(err, "Failed to create JWT for result callback", 0))
			return
		}
	} else {
		bts, err := json.Marshal(result)
		if err != nil {
			_ = server.LogError(errors.WrapPrefix(err, "Failed to marshal session result for result callback", 0))
			return
		}
		res = string(bts)
608 609 610
	}

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