server.go 8.88 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

import (
5
	"fmt"
6
7
	"io/ioutil"
	"net/http"
8
	"time"
9

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

18
var (
19
20
	serv, clientserv *http.Server
	conf             *Configuration
21
)
22

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

29
30
31
	// Start server(s)
	if conf.separateClientServer() {
		go startClientServer()
32
	}
33
	startRequestorServer()
34

35
	return nil
36
37
}

38
39
40
func startRequestorServer() {
	serv = &http.Server{}
	startServer(serv, Handler(), "Server", conf.ListenAddress, conf.Port)
41
42
}

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
func startClientServer() {
	clientserv = &http.Server{}
	startServer(clientserv, ClientHandler(), "Client server", conf.ClientListenAddress, conf.ClientPort)
}

func startServer(s *http.Server, handler http.Handler, name, addr string, port int) {
	fulladdr := fmt.Sprintf("%s:%d", addr, port)
	conf.Logger.Info(name, " listening at ", fulladdr)
	s.Addr = fulladdr
	s.Handler = handler
	if err := s.ListenAndServe(); err != http.ErrServerClosed {
		_ = server.LogFatal(err)
	}
}

func Stop() error {
	var err1, err2 error

	// Even if closing serv fails, we want to try closing clientserv
	err1 = serv.Close()
	if clientserv != nil {
		err2 = clientserv.Close()
	}

	// Now check errors
	if err1 != nil {
		return err1
	}
	if err2 != nil {
		return err2
	}
	return nil
}

func Initialize(config *Configuration) error {
78
	conf = config
79
	if err := irmarequestor.Initialize(conf.Configuration); err != nil {
80
		return err
81
	}
82
	if err := conf.initialize(); err != nil {
83
		return err
84
	}
85
86
	return nil
}
87

88
func ClientHandler() http.Handler {
89
	router := chi.NewRouter()
Sietse Ringers's avatar
Sietse Ringers committed
90
91
92
	router.Use(cors.New(cors.Options{
		AllowedOrigins: []string{"*"},
		AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
Sietse Ringers's avatar
Sietse Ringers committed
93
		AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
Sietse Ringers's avatar
Sietse Ringers committed
94
95
	}).Handler)

96
	router.Mount("/irma/", irmarequestor.HttpHandlerFunc())
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
	return router
}

// Handler returns a http.Handler that handles all IRMA requestor messages
// and IRMA client messages.
func Handler() http.Handler {
	router := chi.NewRouter()
	router.Use(cors.New(cors.Options{
		AllowedOrigins: []string{"*"},
		AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
		AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
	}).Handler)

	if !conf.separateClientServer() {
		// Mount server for irmaclient
		router.Mount("/irma/", irmarequestor.HttpHandlerFunc())
	}
114

115
	// Server routes
116
	router.Post("/session", handleCreate)
117
	router.Delete("/session/{token}", handleDelete)
118
119
	router.Get("/session/{token}/status", handleStatus)
	router.Get("/session/{token}/result", handleResult)
Sietse Ringers's avatar
Sietse Ringers committed
120
121

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

125
	return router
126
127
}

128
129
130
func handleCreate(w http.ResponseWriter, r *http.Request) {
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
131
132
		conf.Logger.Error("Could not read session request HTTP POST body")
		_ = server.LogError(err)
Sietse Ringers's avatar
Sietse Ringers committed
133
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
134
135
		return
	}
136

Sietse Ringers's avatar
Sietse Ringers committed
137
138
139
	// 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.
140
	var (
141
		rrequest  irma.RequestorRequest
142
143
144
145
146
147
		request   irma.SessionRequest
		requestor string
		rerr      *irma.RemoteError
		applies   bool
	)
	for _, authenticator := range authenticators {
148
		applies, rrequest, requestor, rerr = authenticator.Authenticate(r.Header, body)
149
150
151
152
153
		if applies || rerr != nil {
			break
		}
	}
	if rerr != nil {
154
		_ = server.LogError(rerr)
155
156
157
158
		server.WriteResponse(w, nil, rerr)
		return
	}
	if !applies {
159
160
		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
161
		server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
162
163
164
		return
	}

165
166
	// Authorize request: check if the requestor is allowed to verify or issue
	// the requested attributes or credentials
167
	request = rrequest.SessionRequest()
168
169
170
	if request.Action() == irma.ActionIssuing {
		allowed, reason := conf.CanIssue(requestor, request.(*irma.IssuanceRequest).Credentials)
		if !allowed {
171
172
			conf.Logger.Warn("Requestor %s tried to issue credential %s but it is not authorized to; full request: %s",
				requestor, reason, server.ToJson(request))
173
174
175
176
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}
Sietse Ringers's avatar
Sietse Ringers committed
177
	disjunctions := request.ToDisclose()
178
179
180
	if len(disjunctions) > 0 {
		allowed, reason := conf.CanVerifyOrSign(requestor, request.Action(), disjunctions)
		if !allowed {
181
182
			conf.Logger.Warn("Requestor %s tried to verify attribute %s but it is not authorized to; full request: %s",
				requestor, reason, server.ToJson(request))
183
184
185
186
187
188
			server.WriteError(w, server.ErrorUnauthorized, reason)
			return
		}
	}

	// Everything is authenticated and parsed, we're good to go!
189
	qr, _, err := irmarequestor.StartSession(rrequest, nil)
190
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
191
		server.WriteError(w, server.ErrorInvalidRequest, err.Error())
192
193
194
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
195
	server.WriteJson(w, qr)
196
197
198
199
200
}

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
201
		server.WriteError(w, server.ErrorSessionUnknown, "")
202
203
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
204
	server.WriteJson(w, res.Status)
205
206
}

207
208
209
210
211
212
213
func handleDelete(w http.ResponseWriter, r *http.Request) {
	err := irmarequestor.CancelSession(chi.URLParam(r, "token"))
	if err != nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
	}
}

214
215
216
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
217
		server.WriteError(w, server.ErrorSessionUnknown, "")
218
219
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
220
	server.WriteJson(w, res)
221
222
}

223
func handleJwtResult(w http.ResponseWriter, r *http.Request) {
224
	if conf.jwtPrivateKey == nil {
225
		conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
226
227
		server.WriteError(w, server.ErrorUnknown, "JWT signing not supported")
		return
228
	}
229
230
231
232
233

	res := irmarequestor.GetSessionResult(chi.URLParam(r, "token"))
	if res == nil {
		server.WriteError(w, server.ErrorSessionUnknown, "")
		return
234
	}
235
236
237
238
239
240
241
242
243
244
245
246
247

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

func handleJwtProofs(w http.ResponseWriter, r *http.Request) {
259
	if conf.jwtPrivateKey == nil {
260
		conf.Logger.Warn("Session result JWT requested but no JWT private key is configured")
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
		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)
303
	resultJwt, err := token.SignedString(conf.jwtPrivateKey)
304
	if err != nil {
305
306
		conf.Logger.Error("Failed to sign session result JWT")
		_ = server.LogError(err)
307
308
		server.WriteError(w, server.ErrorUnknown, err.Error())
		return
309
	}
310
	server.WriteString(w, resultJwt)
311
}