server.go 7.68 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
5
6

import (
	"io/ioutil"
	"net/http"
7
	"time"
8

9
	"github.com/dgrijalva/jwt-go"
10
	"github.com/go-chi/chi"
Sietse Ringers's avatar
Sietse Ringers committed
11
	"github.com/go-chi/cors"
12
	"github.com/privacybydesign/irmago"
Sietse Ringers's avatar
Sietse Ringers committed
13
14
	"github.com/privacybydesign/irmago/server"
	"github.com/privacybydesign/irmago/server/irmarequestor"
15
16
)

17
18
19
20
var (
	s    *http.Server
	conf *Configuration
)
21

22
// Start the server. If successful then it will not return until Stop() is called.
23
24
func Start(config *Configuration) error {
	handler, err := Handler(config)
25
	if err != nil {
26
27
28
		return err
	}

29
	// Start server
30
31
32
	addr := config.listenAddress()
	config.Logger.Info("Listening at ", addr)
	s = &http.Server{Addr: addr, Handler: handler}
33
34
35
36
37
	err = s.ListenAndServe()
	if err == http.ErrServerClosed {
		return nil // Server was closed normally
	}

38
	return server.LogError(err)
39
40
41
42
43
44
}

func Stop() {
	s.Close()
}

Sietse Ringers's avatar
Sietse Ringers committed
45
46
// Handler returns a http.Handler that handles all IRMA requestor messages
// and IRMA client messages.
47
48
func Handler(config *Configuration) (http.Handler, error) {
	conf = config
49
	if err := conf.initialize(); err != nil {
50
		return nil, server.LogError(err)
51
	}
52
	if err := irmarequestor.Initialize(conf.Configuration); err != nil {
53
54
55
		return nil, err
	}

56
	router := chi.NewRouter()
57

Sietse Ringers's avatar
Sietse Ringers committed
58
59
60
	router.Use(cors.New(cors.Options{
		AllowedOrigins: []string{"*"},
		AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
Sietse Ringers's avatar
Sietse Ringers committed
61
		AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
Sietse Ringers's avatar
Sietse Ringers committed
62
63
	}).Handler)

64
	// Mount server for irmaclient
65
	router.Mount("/irma/", irmarequestor.HttpHandlerFunc())
66

67
	// Server routes
68
	router.Post("/session", handleCreate)
69
	router.Delete("/session/{token}", handleDelete)
70
71
	router.Get("/session/{token}/status", handleStatus)
	router.Get("/session/{token}/result", handleResult)
Sietse Ringers's avatar
Sietse Ringers committed
72
73

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

77
	return router, nil
78
79
}

80
81
82
func handleCreate(w http.ResponseWriter, r *http.Request) {
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
83
84
		conf.Logger.Error("Could not read session request HTTP POST body")
		_ = server.LogError(err)
Sietse Ringers's avatar
Sietse Ringers committed
85
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
86
87
		return
	}
88

Sietse Ringers's avatar
Sietse Ringers committed
89
90
91
	// 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.
92
93
94
95
96
97
98
99
100
101
102
103
104
	var (
		request   irma.SessionRequest
		requestor string
		rerr      *irma.RemoteError
		applies   bool
	)
	for _, authenticator := range authenticators {
		applies, request, requestor, rerr = authenticator.Authenticate(r.Header, body)
		if applies || rerr != nil {
			break
		}
	}
	if rerr != nil {
105
		_ = server.LogError(rerr)
106
107
108
109
		server.WriteResponse(w, nil, rerr)
		return
	}
	if !applies {
110
111
		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
112
		server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
113
114
115
		return
	}

116
117
118
119
120
	// Authorize request: check if the requestor is allowed to verify or issue
	// the requested attributes or credentials
	if request.Action() == irma.ActionIssuing {
		allowed, reason := conf.CanIssue(requestor, request.(*irma.IssuanceRequest).Credentials)
		if !allowed {
121
122
			conf.Logger.Warn("Requestor %s tried to issue credential %s but it is not authorized to; full request: %s",
				requestor, reason, server.ToJson(request))
123
124
125
126
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
Sietse Ringers's avatar
Sietse Ringers committed
127
	disjunctions := request.ToDisclose()
128
129
130
	if len(disjunctions) > 0 {
		allowed, reason := conf.CanVerifyOrSign(requestor, request.Action(), disjunctions)
		if !allowed {
131
132
			conf.Logger.Warn("Requestor %s tried to verify attribute %s but it is not authorized to; full request: %s",
				requestor, reason, server.ToJson(request))
133
134
135
136
137
138
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}

	// Everything is authenticated and parsed, we're good to go!
139
140
	qr, _, err := irmarequestor.StartSession(request, nil)
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
141
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
142
143
144
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
145
	server.WriteJson(w, qr)
146
147
148
149
150
}

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
151
		server.WriteError(w, server.ErrorSessionUnknown, "")
152
153
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
154
	server.WriteJson(w, res.Status)
155
156
}

157
158
159
160
161
162
163
func handleDelete(w http.ResponseWriter, r *http.Request) {
	err := irmarequestor.CancelSession(chi.URLParam(r, "token"))
	if err != nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
	}
}

164
165
166
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
167
		server.WriteError(w, server.ErrorSessionUnknown, "")
168
169
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
170
	server.WriteJson(w, res)
171
172
}

173
func handleJwtResult(w http.ResponseWriter, r *http.Request) {
174
	if conf.jwtPrivateKey == nil {
175
		conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
176
177
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
178
	}
179
180
181
182
183

	res := irmarequestor.GetSessionResult(chi.URLParam(r, "token"))
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
184
	}
185
186
187
188
189
190
191
192
193
194
195
196
197

	claims := struct {
		jwt.StandardClaims
		*server.SessionResult
	}{
		SessionResult: res,
	}
	claims.Issuer = conf.JwtIssuer
	claims.IssuedAt = time.Now().Unix()
	claims.Subject = string(res.Type) + "_result"

	// Sign the jwt and return it
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
198
	resultJwt, err := token.SignedString(conf.jwtPrivateKey)
199
	if err != nil {
200
201
		conf.Logger.Error("Failed to sign session result JWT")
		_ = server.LogError(err)
202
203
204
205
206
207
208
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
	}
	server.WriteString(w, resultJwt)
}

func handleJwtProofs(w http.ResponseWriter, r *http.Request) {
209
	if conf.jwtPrivateKey == nil {
210
		conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
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
245
246
247
248
249
250
251
252
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
	}

	res := irmarequestor.GetSessionResult(chi.URLParam(r, "token"))
	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

	// 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)
253
	resultJwt, err := token.SignedString(conf.jwtPrivateKey)
254
	if err != nil {
255
256
		conf.Logger.Error("Failed to sign session result JWT")
		_ = server.LogError(err)
257
258
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
259
	}
260
	server.WriteString(w, resultJwt)
261
}