1 /* zxiduma.c - Handwritten nitty-gritty functions for UMA
2 * Copyright (c) 2011-2014 Sampo Kellomaki (sampo@iki.fi), All Rights Reserved.
3 * This is confidential unpublished proprietary source code of the author.
4 * NO WARRANTY, not even implied warranties. Contains trade secrets.
5 * Distribution prohibited unless authorized in writing.
6 * Licensed under Apache License 2.0, see file COPYING.
7 * $Id$
8 *
9 * While this file contains some protocol encoders and decoders for OAUTH2,
10 * the main logic of the flows is integrated to other parts, such as zxidsimp.c
11 *
12 * http://openid.net/specs/openid-connect-basic-1_0.html
13 * http://openid.net/specs/openid-connect-session-1_0.html
14 * http://openid.net/specs/openid-connect-messages-1_0.html
15 * http://tools.ietf.org/html/draft-ietf-oauth-v2-22
16 * http://tools.ietf.org/html/draft-jones-json-web-encryption-01
17 *
18 * 11.12.2011, created --Sampo
19 * 9.10.2014, adapted from zxidoauth.c --Sampo
20 */
21
22 #include "platform.h"
23 #include "errmac.h"
24 #include "zx.h"
25 #include "zxid.h"
26 #include "zxidpriv.h"
27 #include "zxidutil.h"
28 #include "zxidconf.h"
29 #include "saml2.h" /* for bindings like OAUTH2_REDIR */
30 #include "c/zx-data.h"
31
32 /*() Interpret ZXID standard form fields to construct a XML structure for AuthnRequest */
33
34 /* Called by: zxid_start_sso_url */
zxid_mk_oauth_az_req(zxid_conf * cf,zxid_cgi * cgi,struct zx_str * loc,char * relay_state)35 struct zx_str* zxid_mk_oauth_az_req(zxid_conf* cf, zxid_cgi* cgi, struct zx_str* loc, char* relay_state)
36 {
37 struct zx_str* ss;
38 struct zx_str* nonce;
39 struct zx_str* eid;
40 char* eid_url_enc;
41 char* redir_url_enc;
42 char* state_b64;
43 char* prompt;
44 char* display;
45
46 if (!loc) {
47 ERR("Redirection location URL missing. %d", 0);
48 return 0;
49 }
50
51 redir_url_enc = zx_url_encode(cf->ctx, strlen(cf->burl), cf->burl, 0);
52 eid = zxid_my_ent_id(cf);
53 eid_url_enc = zx_url_encode(cf->ctx, eid->len, eid->s, 0);
54 zx_str_free(cf->ctx, eid);
55
56 if (relay_state)
57 state_b64 = zxid_deflate_safe_b64_raw(cf->ctx, strlen(relay_state), relay_state);
58 else
59 state_b64 = 0;
60 nonce = zxid_mk_id(cf, "OA", ZXID_ID_BITS);
61 prompt = BOOL_STR_TEST(cgi->force_authn) ? "login" : 0;
62 prompt = BOOL_STR_TEST(cgi->consent && cgi->consent[0]) ? (prompt?"login+consent":"consent") : prompt;
63 display = BOOL_STR_TEST(cgi->ispassive) ? "none" : 0;
64
65 ss = zx_strf(cf->ctx,
66 "%.*s%cresponse_type=token+id_token"
67 "&client_id=%s"
68 "&scope=openid+profile+email+address"
69 "&redirect_uri=%s%%3fo=O"
70 "&nonce=%.*s"
71 "%s%s" /* &state= */
72 "%s%s" /* &display= */
73 "%s%s" /* &prompt= */
74 CRLF2,
75 loc->len, loc->s, (memchr(loc->s, '?', loc->len)?'&':'?'),
76 eid_url_enc,
77 redir_url_enc,
78 nonce->len, nonce->s,
79 state_b64?"&state=":"", STRNULLCHK(state_b64),
80 display?"&display=":"", STRNULLCHK(display),
81 prompt?"&prompt=":"", STRNULLCHK(prompt)
82 );
83 D("OAUTH2 AZ REQ(%.*s)", ss->len, ss->s);
84 if (errmac_debug & ERRMAC_INOUT) INFO("%.*s", ss->len, ss->s);
85 zx_str_free(cf->ctx, nonce);
86 ZX_FREE(cf->ctx, state_b64);
87 ZX_FREE(cf->ctx, eid_url_enc);
88 ZX_FREE(cf->ctx, redir_url_enc);
89 return ss;
90 }
91
92 /*() Construct OAUTH2 / OpenID-Connect1 id_token. */
93
94 /* Called by: zxid_sso_issue_jwt */
zxid_mk_jwt(zxid_conf * cf,int claims_len,char * claims)95 char* zxid_mk_jwt(zxid_conf* cf, int claims_len, char* claims)
96 {
97 char hash[64 /*EVP_MAX_MD_SIZE*/];
98 char* jwt_hdr;
99 int hdr_len;
100 char* b64;
101 char* p;
102 int len = SIMPLE_BASE64_LEN(claims_len);
103
104 switch (cf->oaz_jwt_sigenc_alg) {
105 case 'n':
106 jwt_hdr = "{\"typ\":\"JWT\",\"alg\":\"none\"}";
107 hdr_len = strlen(jwt_hdr);
108 len += SIMPLE_BASE64_LEN(hdr_len) + 1 + 1;
109 break;
110 case 'h':
111 jwt_hdr = "{\"typ\":\"JWT\",\"alg\":\"HS256\"}";
112 hdr_len = strlen(jwt_hdr);
113 len += SIMPLE_BASE64_LEN(hdr_len) + 1 + 1 + 86 /* siglen conservative estimate */;
114 break;
115 case 'r':
116 jwt_hdr = "{\"typ\":\"JWT\",\"alg\":\"RS256\"}";
117 hdr_len = strlen(jwt_hdr);
118 len += SIMPLE_BASE64_LEN(hdr_len) + 1 + 1 + 500 /* siglen conservative estimate */;
119 break;
120 default:
121 ERR("Unrecognized OAZ_JWT_SIGENC_ALG spec(%c). See zxid-conf.pd or zxidconf.h for documentation.", cf->oaz_jwt_sigenc_alg);
122 return 0;
123 }
124
125 b64 = ZX_ALLOC(cf->ctx, len+1);
126 p = base64_fancy_raw(jwt_hdr, hdr_len, b64, safe_basis_64, 1<<31, 0, 0, 0);
127 *p++ = '.';
128 p = base64_fancy_raw(claims, claims_len, p, safe_basis_64, 1<<31, 0, 0, 0);
129 *p = 0;
130
131 switch (cf->oaz_jwt_sigenc_alg) {
132 case 'n':
133 *p++ = '.';
134 *p = 0;
135 break;
136 case 'h':
137 if (!cf->hmac_key[0])
138 zx_get_symkey(cf, "hmac.key", cf->hmac_key);
139 zx_hmac_sha256(cf->ctx, ZX_SYMKEY_LEN, cf->hmac_key, p-b64, b64, hash, &len);
140 *p++ = '.';
141 p = base64_fancy_raw(hash, len, p, safe_basis_64, 1<<31, 0, 0, 0);
142 *p = 0;
143 break;
144 case 'r':
145 ERR("RSA not implemented yet %d",0);
146 zx_hmac_sha256(cf->ctx, ZX_SYMKEY_LEN, cf->hmac_key, p-b64, b64, hash, &len);
147 *p++ = '.';
148 p = base64_fancy_raw(hash, len, p, safe_basis_64, 1<<31, 0, 0, 0);
149 *p = 0;
150 break;
151 }
152 D("JWT(%s)", b64);
153 return b64;
154 }
155
156 /*() Issue OAUTH2 / OpenID-Connect1 id_token. */
157
158 /* Called by: zxid_oauth2_az_server_sso */
zxid_sso_issue_jwt(zxid_conf * cf,zxid_cgi * cgi,zxid_ses * ses,struct timeval * srcts,zxid_entity * sp_meta,struct zx_str * acsurl,zxid_nid ** nameid,char * logop)159 char* zxid_sso_issue_jwt(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses, struct timeval* srcts, zxid_entity* sp_meta, struct zx_str* acsurl, zxid_nid** nameid, char* logop)
160 {
161 int rawlen;
162 char* buf;
163 char* jwt;
164 char* jwt_id; /* sha1 hash of the jwt, taken from log_path */
165 struct zx_str issuer;
166 struct zx_str* affil;
167 char* eid;
168 struct zx_str* logpath;
169 struct zx_str ss;
170 struct zx_str nn;
171 struct zx_str id;
172 zxid_nid* tmpnameid;
173 char sp_name_buf[ZXID_MAX_SP_NAME_BUF];
174 D("sp_eid(%s)", sp_meta->eid);
175 if (!nameid)
176 nameid = &tmpnameid;
177
178 //if (ar && ar->IssueInstant && ar->IssueInstant->g.len && ar->IssueInstant->g.s)
179 // srcts->tv_sec = zx_date_time_to_secs(ar->IssueInstant->g.s);
180
181 if (!cgi->allow_create)
182 cgi->allow_create = '1';
183 if (!cgi->nid_fmt || !cgi->nid_fmt[0])
184 cgi->nid_fmt = "prstnt"; /* Persistent is the default implied by the specs. */
185
186 /* Check for federation. */
187
188 issuer.s = cgi->client_id; issuer.len = strlen(cgi->client_id);
189 affil = &issuer;
190 zxid_nice_sha1(cf, sp_name_buf, sizeof(sp_name_buf), affil, affil, 7);
191 D("sp_name_buf(%s) allow_create=%d", sp_name_buf, cgi->allow_create);
192
193 *nameid = zxid_get_fed_nameid(cf, &issuer, affil, ses->uid, sp_name_buf, cgi->allow_create,
194 (cgi->nid_fmt && !strcmp(cgi->nid_fmt, "trnsnt")),
195 srcts, 0, logop);
196 if (logop) { logop[3]='S'; logop[4]='S'; logop[5]='O'; logop[6]=0; /* Patch in SSO */ }
197 if (!*nameid) {
198 ERR("get_fed_nameid() client_id(%s) returned NULL", cgi->client_id);
199 return 0;
200 }
201
202 eid = zxid_my_ent_id_cstr(cf);
203 // ,\"\":\"\"
204 buf = zx_alloc_sprintf(cf->ctx, &rawlen,
205 "{\"iss\":\"%s\""
206 ",\"user_id\":\"%.*s\""
207 ",\"aud\":\"%s\""
208 ",\"exp\":%d"
209 ",\"nonce\":\"%s\"}",
210 eid,
211 ZX_GET_CONTENT_LEN(*nameid), ZX_GET_CONTENT_S(*nameid),
212 cgi->client_id,
213 time(0) + cf->timeskew + cf->a7nttl,
214 cgi->nonce);
215 ZX_FREE(cf->ctx, eid);
216 jwt = zxid_mk_jwt(cf, rawlen, buf);
217 ZX_FREE(cf->ctx, buf);
218
219 /* Log the issued JWT */
220
221 ss.s = jwt; ss.len = strlen(jwt);
222 logpath = zxlog_path(cf, &issuer, &ss, ZXLOG_ISSUE_DIR, ZXLOG_JWT_KIND, 1);
223 if (!logpath) {
224 ERR("Could not generate logpath for aud(%s) JWT(%s)", cgi->client_id, jwt);
225 ZX_FREE(cf->ctx, jwt);
226 return 0;
227 }
228
229 /* Since JWT does not have explicit ID attribute, we use the sha1 hash of the
230 * contents of JWT as an ID. Since this is what logpath also does, we just
231 * use the last component of the logpath. */
232 for (jwt_id = logpath->s + logpath->len; jwt_id > logpath->s && jwt_id[-1] != '/'; --jwt_id) ;
233
234 if (cf->log_issue_a7n) {
235 if (zxlog_dup_check(cf, logpath, "sso_issue_jwt")) {
236 ERR("Duplicate JWT ID(%s)", jwt_id);
237 if (cf->dup_a7n_fatal) {
238 ERR("FATAL (by configuration): Duplicate JWT ID(%s)", jwt_id);
239 zxlog_blob(cf, 1, logpath, &ss, "sso_issue_JWT dup");
240 zx_str_free(cf->ctx, logpath);
241 ZX_FREE(cf->ctx, jwt);
242 return 0;
243 }
244 }
245 zxlog_blob(cf, 1, logpath, &ss, "sso_issue_JWT");
246 }
247
248 nn.s = cgi->nonce; nn.len = strlen(cgi->nonce);
249 id.s = jwt_id; id.len = strlen(jwt_id);
250
251 if (cf->loguser)
252 zxlogusr(cf, ses->uid, 0, 0, 0, &issuer, &nn, &id,
253 ZX_GET_CONTENT(*nameid),
254 (cf->oaz_jwt_sigenc_alg!='n'?"U":"N"), "K", logop, 0, 0);
255
256 zxlog(cf, 0, 0, 0, &issuer, &nn, &id,
257 ZX_GET_CONTENT(*nameid),
258 (cf->oaz_jwt_sigenc_alg!='n'?"U":"N"), "K", logop, 0, 0);
259
260 zx_str_free(cf->ctx, logpath);
261 return jwt;
262 }
263
264 /*(i) Generate SSO assertion and ship it to SP by OAUTH2 Az redir binding. User has already
265 * logged in by the time this is called. See also zxid_ssos_anreq() and zxid_idp_sso(). */
266
267 /* Called by: zxid_idp_dispatch */
zxid_oauth2_az_server_sso(zxid_conf * cf,zxid_cgi * cgi,zxid_ses * ses)268 struct zx_str* zxid_oauth2_az_server_sso(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses)
269 {
270 zxid_entity* sp_meta;
271 struct zx_str* acsurl = 0;
272 struct timeval srcts = {0,501000};
273 zxid_nid* nameid;
274 char* idtok;
275 char logop[8];
276 strcpy(logop, "OAZxxxx");
277
278 if (!cgi->client_id || !cgi->redirect_uri || !cgi->nonce) {
279 ERR("Missing mandatory OAUTH2 field client_id=%p redirect_uri=%p nonce=%p", cgi->client_id, cgi->redirect_uri, cgi->nonce);
280 return zx_dup_str(cf->ctx, "* ERR");
281 }
282
283 if (!cgi->response_type || !strstr(cgi->response_type, "token") || !strstr(cgi->response_type, "id_token")) {
284 ERR("Missing mandatory OAUTH2 field response_type(%s) missing or does not contain `token id_token'", STRNULLCHKD(cgi->response_type));
285 return zx_dup_str(cf->ctx, "* ERR");
286 }
287
288 if (!cgi->scope || !strstr(cgi->scope, "openid")) {
289 ERR("Missing mandatory OAUTH2 field scope=%p or the scope does not contain `openid'", STRNULLCHKD(cgi->scope));
290 return zx_dup_str(cf->ctx, "* ERR");
291 }
292
293 sp_meta = zxid_get_ent(cf, cgi->client_id);
294 if (!sp_meta) {
295 ERR("The metadata for client_id(%s) of the Az Req could not be found or fetched", cgi->client_id);
296 return zx_dup_str(cf->ctx, "* ERR");
297 }
298 D("sp_eid(%s)", sp_meta->eid);
299
300 /* Figure out the binding and url */
301
302 acsurl = zxid_sp_loc_raw(cf, cgi, sp_meta, ZXID_ACS_SVC, OAUTH2_REDIR, 0);
303 if (!acsurl) {
304 ERR("sp(%s) metadata does not have SPSSODescriptor/AssertionConsumerService with Binding=\"" OAUTH2_REDIR "\". Pre-registering the SP at IdP is mandatory. redirect_uri(%s) will be ignored. (remote SP metadata problem)", sp_meta->eid, cgi->redirect_uri);
305 return zx_dup_str(cf->ctx, "* ERR");
306 }
307 if (strlen(cgi->redirect_uri) != acsurl->len || memcmp(cgi->redirect_uri, acsurl->s, acsurl->len)) {
308 ERR("sp(%s) metadata has SPSSODescriptor/AssertionConsumerService with Binding=\"" OAUTH2_REDIR "\" has value(%.*s), which is different from redirect_uri(%s). (remote SP problem)", sp_meta->eid, acsurl->len, acsurl->s, cgi->redirect_uri);
309 return zx_dup_str(cf->ctx, "* ERR");
310 }
311
312 if (!cf->log_issue_a7n) {
313 INFO("LOG_ISSUE_A7N must be turned on in IdP configuration for artifact profile to work. Turning on now automatically. %d", 0);
314 cf->log_issue_a7n = 1;
315 }
316
317 /* User ses->uid is already logged in, now check for federation with sp */
318
319 idtok = zxid_sso_issue_jwt(cf, cgi, ses, &srcts, sp_meta, acsurl, &nameid, logop);
320 if (!idtok) {
321 ERR("Issuing JWT Failed %s", logop);
322 return zx_dup_str(cf->ctx, "* ERR");
323 }
324
325 D("OAUTH2-ART ep(%.*s)", acsurl->len, acsurl->s);
326 zxlog(cf, 0, &srcts, 0, sp_meta->ed?&sp_meta->ed->entityID->g:0, 0, 0, ZX_GET_CONTENT(nameid), "N", "K", logop, ses->uid, "OAUTH2-ART");
327
328 /* Formulate OAUTH2 / OpenID-Connect1 Az Redir Response */
329
330 return zx_strf(cf->ctx, "Location: %s%c"
331 "access_token=%s"
332 "&token_type=bearer"
333 "&id_token=%s"
334 "&expires_in=%d" CRLF
335 "%s%s%s", /* Set-Cookie */
336 cgi->redirect_uri, (strchr(cgi->redirect_uri, '?') ? '&' : '?'),
337 idtok,
338 idtok,
339 cf->a7nttl,
340 ses->setcookie?"Set-Cookie: ":"", ses->setcookie?ses->setcookie:"", ses->setcookie?CRLF:"");
341 }
342
343 /*() Extract an assertion from OAUTH Az response, and perform SSO */
344
345 /* Called by: zxid_sp_oauth2_dispatch */
zxid_sp_dig_oauth_sso_a7n(zxid_conf * cf,zxid_cgi * cgi,zxid_ses * ses)346 static int zxid_sp_dig_oauth_sso_a7n(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses)
347 {
348 //if (!zxid_chk_sig(cf, cgi, ses, &resp->gg, resp->Signature, resp->Issuer, 0, "Response")) return 0;
349
350 //p = zxid_http_get(cf, url, &lim);
351
352 ERR("*** process JWT %d", 0);
353
354 //a7n = zxid_dec_a7n(cf, resp->Assertion, resp->EncryptedAssertion);
355 //if (a7n) {
356 // zx_see_elem_ns(cf->ctx, &pop_seen, &resp->gg);
357 // return zxid_sp_sso_finalize(cf, cgi, ses, a7n, pop_seen);
358 //}
359 if (cf->anon_ok && cgi->rs && !strcmp(cf->anon_ok, cgi->rs)) /* Prefix match */
360 return zxid_sp_anon_finalize(cf, cgi, ses);
361 ERR("No Assertion found and not anon_ok in OAUTH Response %d", 0);
362 zxlog(cf, 0, 0, 0, 0, 0, 0, ZX_GET_CONTENT(ses->nameid), "N", "C", "ERR", 0, "sid(%s) No assertion", ses->sid?ses->sid:"");
363 return 0;
364 }
365
366 /*() Dispatch, on RP/SP side, OAUTH redir or artifact binding requests.
367 *
368 * return:: a string (such as Location: header) and let the caller output it.
369 * Sometimes a dummy string is just output to indicate status, e.g.
370 * "O" for SSO OK, "K" for normal OK no further action needed,
371 * "M" show management screen, "I" forward to IdP dispatch, or
372 * "* ERR" for error situations. These special strings
373 * are allocated from static storage and MUST NOT be freed. Other
374 * strings such as "Location: ..." should be freed by caller. */
375
376 /* Called by: zxid_simple_no_ses_cf */
zxid_sp_oauth2_dispatch(zxid_conf * cf,zxid_cgi * cgi,zxid_ses * ses)377 struct zx_str* zxid_sp_oauth2_dispatch(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses)
378 {
379 int ret;
380
381 if (cgi->id_token) { /* OAUTH2 artifact / redir biding, aka OpenID-Connect1 */
382 ret = zxid_sp_dig_oauth_sso_a7n(cf, cgi, ses);
383 D("ret=%d ses=%p", ret, ses);
384 switch (ret) {
385 case ZXID_OK: return zx_dup_str(cf->ctx, "K");
386 case ZXID_SSO_OK: return zx_dup_str(cf->ctx, "O");
387 case ZXID_IDP_REQ: /* (PXY) Middle IdP of IdP Proxy flow */
388 return zx_dup_str(cf->ctx, zxid_simple_ses_active_cf(cf, cgi, ses, 0, 0x1fff));
389 case ZXID_FAIL:
390 D("*** FAIL, should send back to IdP select %d", 0);
391 return zx_dup_str(cf->ctx, "* ERR");
392 }
393 return zx_dup_str(cf->ctx, "M"); /* Management screen, please. */
394 }
395
396 if (cf->log_level > 0)
397 zxlog(cf, 0, 0, 0, 0, 0, 0, ZX_GET_CONTENT(ses->nameid), "N", "C", "SPOADISP", 0, "sid(%s) unknown req or resp", STRNULLCHK(ses->sid));
398 ERR("Unknown request or response %d", 0);
399 return zx_dup_str(cf->ctx, "* ERR");
400 }
401
402 /*() Handle, on IdP side, OAUTH2 / OpenID-Connect1 check_id requests.
403 *
404 * return:: a string (such as Location: header) and let the caller output it.
405 * Sometimes a dummy string is just output to indicate status, e.g.
406 * "O" for SSO OK, "K" for normal OK no further action needed,
407 * "M" show management screen, "I" forward to IdP dispatch, or
408 * "* ERR" for error situations. These special strings
409 * are allocated from static storage and MUST NOT be freed. Other
410 * strings such as "Location: ..." should be freed by caller. */
411
412 /* Called by: zxid_simple_no_ses_cf */
zxid_idp_oauth2_check_id(zxid_conf * cf,zxid_cgi * cgi,zxid_ses * ses,int auto_flags)413 char* zxid_idp_oauth2_check_id(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses, int auto_flags)
414 {
415 int ret = 0;
416
417 if (cgi->id_token) { /* OAUTH2 artifact / redir biding, aka OpenID-Connect1 */
418 /* The id_token is directly the local filename of the corresponsing assertion. */
419
420 D("ret=%d ses=%p", ret, ses);
421
422 //return zxid_simple_show_page(cf, ss, ZXID_AUTO_METAC, ZXID_AUTO_METAH, "b", "text/xml", res_len, auto_flags, 0);
423 }
424
425 if (cf->log_level > 0)
426 zxlog(cf, 0, 0, 0, 0, 0, 0, ZX_GET_CONTENT(ses->nameid), "N", "C", "IDPOACI", 0, "sid(%s) unknown req or resp", STRNULLCHK(ses->sid));
427 ERR("Unknown request or response %d", 0);
428 return 0;
429 }
430
431 /* EOF -- zxiduma.c */
432