From b43e782b121555af4fcd000dee4ceb40865dbc0c Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Mon, 10 Feb 2020 15:32:13 -0700 Subject: [PATCH] Add support for RFC6750 bearer tokens to ocserv This permits the validation of OpenID Connect auth tokens OpenID Connect is an OAuth 2.0 protocol used to identify a resource owner (VPN client end-user) to a resource server (VPN server) intermediated by an Authorization server. Resolves: #240 Signed-off-by: Alan TG Jowett --- .gitlab-ci.yml | 6 +- configure.ac | 18 + doc/README-oidc.md | 87 +++++ src/Makefile.am | 17 +- src/auth/openidconnect.c | 622 +++++++++++++++++++++++++++++++ src/auth/openidconnect.h | 23 ++ src/common-config.h | 1 + src/config.c | 7 + src/sec-mod-auth.c | 7 +- src/subconfig.c | 19 + src/vpn.h | 5 +- src/worker-auth.c | 68 +++- tests/Makefile.am | 11 +- tests/config-auth.xml | 7 + tests/data/test-oidc-auth.config | 198 ++++++++++ tests/generate_oidc_test_data.c | 316 ++++++++++++++++ tests/test-oidc | 55 +++ 17 files changed, 1456 insertions(+), 11 deletions(-) create mode 100644 doc/README-oidc.md create mode 100644 src/auth/openidconnect.c create mode 100644 src/auth/openidconnect.h create mode 100644 tests/config-auth.xml create mode 100644 tests/data/test-oidc-auth.config create mode 100644 tests/generate_oidc_test_data.c create mode 100755 tests/test-oidc diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f482fce5..6b0654d6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ Debian: script: - chmod -R o-w tests/data/raddb - git submodule update --init && autoreconf -fvi && - ./configure --without-nuttcp-tests --without-docker-tests && make -j$JOBS && make check -j$JOBS + ./configure --without-nuttcp-tests --without-docker-tests --enable-oidc-auth && make -j$JOBS && make check -j$JOBS tags: - shared - linux @@ -61,7 +61,7 @@ Ubuntu18.04: script: - git submodule update --init - autoreconf -fvi - - ./configure --without-nuttcp-tests --without-docker-tests + - ./configure --without-nuttcp-tests --without-docker-tests --enable-oidc-auth - make -j$JOBS - make check -j$JOBS tags: @@ -231,7 +231,7 @@ Fedora: - chmod -R o-w tests/data/raddb - git submodule update --init - autoreconf -fvi - - CFLAGS="-g -O0" ./configure --disable-maintainer-mode --without-docker-tests --with-werror --enable-code-coverage --enable-kerberos-tests + - CFLAGS="-g -O0" ./configure --disable-maintainer-mode --without-docker-tests --with-werror --enable-code-coverage --enable-kerberos-tests --enable-oidc-auth - make -j$JOBS - make check -j$JOBS COVERAGE=1 - make files-update diff --git a/configure.ac b/configure.ac index 37660561..46785539 100644 --- a/configure.ac +++ b/configure.ac @@ -577,6 +577,23 @@ if test "$have_cwrap_pam" = yes; then fi fi +AC_ARG_ENABLE([oidc-auth], + [AS_HELP_STRING([--oidc-auth], + [whether to support OpenID Connect auth (default is no)])], + [enable_oidc_auth=$enableval], + [enable_oidc_auth=no] +) + +if test "x$enable_oidc_auth" = xyes; then + AC_DEFINE([SUPPORT_OIDC_AUTH], 1, [Enable support for OpenID Connect auth]) + PKG_CHECK_MODULES([LIBCURL], [libcurl]) + PKG_CHECK_MODULES([CJOSE], [cjose]) + PKG_CHECK_MODULES([JANSSON], [jansson]) +fi + +AM_CONDITIONAL(ENABLE_OIDC_AUTH, test "x$enable_oidc_auth" = xyes) +AM_CONDITIONAL(ENABLE_OIDC_AUTH_TESTS, test "x$enable_oidc_auth" = xyes) + uid=$(id -u) gid=$(id -g) AC_SUBST([ROOTUID], [$uid]) @@ -610,6 +627,7 @@ Summary of build options: PAM auth backend: ${pam_enabled} Radius auth backend: ${radius_enabled} GSSAPI auth backend: ${enable_gssapi} + OIDC Auth backend: ${enable_oidc_auth} Anyconnect compat: ${anyconnect_enabled} TCP wrappers: ${libwrap_enabled} systemd: ${systemd_enabled} diff --git a/doc/README-oidc.md b/doc/README-oidc.md new file mode 100644 index 00000000..58b90fe8 --- /dev/null +++ b/doc/README-oidc.md @@ -0,0 +1,87 @@ +# Using ocserv with OpenIDConnect authentication + +OpenID Connect (OIDC) is an identity layer build on top of the OAuth 2.0 protocols. Authentication using OIDC utilizes the following flow: + + +--------+ +---------------+ + | |--(A)- Authorization Request ->| Resource | + | | | Owner | + | |<-(B)-- Authorization Grant ---| (end user) | + | | +---------------+ + | | + | | +---------------+ + | |--(C)-- Authorization Grant -->| Authorization | + | Client | | Server | + | |<-(D)----- Access Token -------|(oidc provide) | + | | +---------------+ + | | + | | +---------------+ + | |--(E)----- Access Token ------>| Resource | + | | | Server | + | |<-(F)--- Protected Resource ---| (ocserv) | + +--------+ +---------------+ + +For as more detailed explanation see the OpenID Connect protocol ( + +## Deploying OIDC authentication + +An administrator wanting to deployg OIDC as an authentication scheme must do the following: + +1) Register an application identity with the OIDC provider +2) Obtain the token endpoint and the OpenID Connect metadata document endpoint for their OIDC provider +3) Determine what claims the OIDC provider supports +4) Author a JSON document tell ocserv how to validate the token +5) Add a line to the ocserv config file pointing to oidc config file: `auth = "oidc[config=]"` + +See your OIDC providers documentation to better understand what claims they support. + +## OIDC JSON Config file + +Oidc.json file format: +```json +{ + "openid_configuration_url": "", + "user_name_claim": "preferred_username", + "required_claims": { + "aud": "SomeAudience", + "iss": "SomeIssuer" + } +} +``` + +Example openid-configuration doc URIs are: +1) +2) + +Required claims controls what claims must be present in a token to permit access. + +See your OpenID Connect provider for details on claims and OpenID Connect metadata document URL. + +## Sample token + +An OIDC token is returned as a base64url encoded blob. +`eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJFUzI1NiIsICJraWQiOiAiTXkgRmFrZSBLZXkifQ.eyJhdWQiOiAiU29tZUF1ZGllbmNlIiwgImlzcyI6ICJTb21lSXNzdWVyIiwgImlhdCI6IDE1ODE5ODAzMzcsICJuYmYiOiAxNTgxOTgwMzM3LCAiZXhwIjogMTU4MTk4Mzk5NywgInByZWZlcnJlZF91c2VybmFtZSI6ICJTb21lVXNlciJ9.dBGYHphmSHx_IQp09LpK9wkxAcIqnNRkX2Z59PPe0q7aU8yr2QZrq2fqtqRgk3fJ-LyRFaL5HyKHOHq3xebdXg` + +You can view the contents of the token using . +``` +{ + "typ": "JWT", + "alg": "ES256", + "kid": "My Fake Key" +}.{ + "aud": "SomeAudience", + "iss": "SomeIssuer", + "iat": 1581980337, + "nbf": 1581980337, + "exp": 1581983997, + "preferred_username": "SomeUser" +}.[Signature] +``` + +|Claim type|Value|Notes| +|--------------|:--------|----:| +|aud|SomeAudience|The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. [RFC 7519, Section 4.1.3]| +|iss|SomeIssuer|The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. [RFC 7519, Section 4.1.1]| +|iat|Mon Feb 17 2020 15:58:57 GMT-0700 (Mountain Standard Time)|The "iat" (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. [RFC 7519, Section 4.1.6]| +|nbf|Mon Feb 17 2020 15:58:57 GMT-0700 (Mountain Standard Time)|The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. [RFC 7519, Section 4.1.5]| +|exp|Mon Feb 17 2020 16:59:57 GMT-0700 (Mountain Standard Time)|The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. [RFC 7519, Section 4.1.4]| +|preferred_username|SomeUser|Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters such as @, /, or whitespace. The RP MUST NOT rely upon this value being unique, as discussed in OpenID Connect Core 1.0 Section 5.7. [OpenID Connect Core 1.0, Section 5.1]| \ No newline at end of file diff --git a/src/Makefile.am b/src/Makefile.am index a60fc968..1587d745 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -8,6 +8,10 @@ AM_CPPFLAGS += -I$(srcdir)/../gl/ -I$(builddir)/../gl/ \ $(LIBTALLOC_CFLAGS) $(LIBDBUS_CFLAGS) $(LIBOATH_CFLAGS) \ $(LIBKRB5_CFLAGS) $(LIBTASN1_CFLAGS) $(RADCLI_CFLAGS) $(SRC_CFLAGS) +if ENABLE_OIDC_AUTH +AM_CPPFLAGS += $(LIBCURL_CFLAGS) $(CJOSE_CFLAGS) $(JANSSON_CFLAGS) +endif + BUILT_SOURCES = ipc.pb-c.c ipc.pb-c.h \ http-heads.h kkdcp_asn1_tab.c ctl.pb-c.c ctl.pb-c.h @@ -23,7 +27,11 @@ noinst_LIBRARIES = libipc.a # Authentication module sources AUTH_SOURCES=auth/pam.c auth/pam.h auth/plain.c auth/plain.h auth/radius.c auth/radius.h \ auth/common.c auth/common.h auth/gssapi.h auth/gssapi.c auth-unix.c \ - auth-unix.h + auth-unix.h + +if ENABLE_OIDC_AUTH +AUTH_SOURCES += auth/openidconnect.c auth/openidconnect.h +endif ACCT_SOURCES=acct/radius.c acct/radius.h acct/pam.c acct/pam.h @@ -69,8 +77,11 @@ ocserv_LDADD += $(LIBGNUTLS_LIBS) $(PAM_LIBS) $(LIBUTIL) \ $(RADCLI_LIBS) $(LIBLZ4_LIBS) $(LIBKRB5_LIBS) \ $(LIBTASN1_LIBS) $(LIBOATH_LIBS) $(LIBNETTLE_LIBS) \ $(LIBEV_LIBS) libipc.a $(NEEDED_LIBPROTOBUF_LIBS) \ - $(CODE_COVERAGE_LDFLAGS) - + $(CODE_COVERAGE_LDFLAGS) + +if ENABLE_OIDC_AUTH +ocserv_LDADD += $(LIBCURL_LIBS) $(CJOSE_LIBS) $(JANSSON_LIBS) +endif ocserv_SOURCES += main-ctl-unix.c diff --git a/src/auth/openidconnect.c b/src/auth/openidconnect.c new file mode 100644 index 00000000..d9ea61fb --- /dev/null +++ b/src/auth/openidconnect.c @@ -0,0 +1,622 @@ +/* + * Copyright (C) 2020 Microsoft Corporation + * + * Author: Alan Jowett + * + * This file is part of ocserv. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see + */ + +#include +#include +#include +#include +#include +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE +#endif +#include +#include +#include +#include "plain.h" +#include "common-config.h" +#include "auth/common.h" + +#ifdef SUPPORT_OIDC_AUTH +#include +#include +#include +#include + +typedef struct oidc_vctx_st { + json_t *config; + json_t *jwks; +} oidc_vctx_st; + +typedef struct oidc_ctx_st { + oidc_vctx_st *vctx_st; + char username[MAX_USERNAME_SIZE]; + int token_verified; +} oidc_ctx_st; + +static bool oidc_fetch_oidc_keys(void * pool, oidc_vctx_st * vctx); +static bool oidc_verify_token(oidc_vctx_st * vctx, const char *token, + size_t token_length, + char user_name[MAX_USERNAME_SIZE]); + +static void oidc_vhost_init(void **vctx, void *pool, void *additional) +{ + const char *config = (const char *)additional; + json_error_t err; + struct oidc_vctx_st *vc; + + vc = talloc(pool, struct oidc_vctx_st); + if (vc == NULL) { + syslog(LOG_ERR, "ocserv-oidc allocation failure!\n"); + exit(1); + } + vc->config = NULL; + vc->jwks = NULL; + + if (config == NULL) { + syslog(LOG_ERR, "ocserv-oidc: no configuration passed!\n"); + exit(1); + } + + vc->config = json_load_file(config, 0, &err); + if (vc->config == NULL) { + syslog(LOG_ERR, "ocserv-oidc: failed to load config file: %s\n", config); + exit(1); + } + + if (!json_object_get(vc->config, "openid_configuration_url")) { + syslog(LOG_ERR, + "ocserv-oidc: config file missing openid_configuration_url\n"); + exit(1); + } + + if (!json_object_get(vc->config, "required_claims")) { + syslog(LOG_ERR, + "ocserv-oidc: config file missing required_claims\n"); + exit(1); + } + + if (!json_object_get(vc->config, "user_name_claim")) { + syslog(LOG_ERR, + "ocserv-oidc: config file missing user_name_claim\n"); + exit(1); + } + + if (!oidc_fetch_oidc_keys(pool, vc)) { + syslog(LOG_ERR, "ocserv-oidc: failed to load jwks\n"); + exit(1); + } + + *vctx = (void *)vc; + + return; +} + +static void oidc_vhost_deinit(void *ctx) +{ + oidc_vctx_st *vctx = (oidc_vctx_st *) ctx; + + if (!vctx) { + return; + } + + if (vctx->jwks) { + json_decref(vctx->jwks); + vctx->jwks = NULL; + } + + if (vctx->config) { + json_decref(vctx->config); + vctx->config = NULL; + } +} + +static int oidc_auth_init(void **ctx, void *pool, void *vctx, + const common_auth_init_st * info) +{ + oidc_vctx_st *vt = (oidc_vctx_st *) vctx; + oidc_ctx_st *ct; + ct = talloc_zero(pool, struct oidc_ctx_st); + if (!ct) { + return ERR_AUTH_FAIL; + } + ct->vctx_st = vt; + *ctx = (void *)ct; + + if (oidc_verify_token(ct->vctx_st, info->username, strlen(info->username), ct->username)) { + ct->token_verified = 1; + return 0; + } else { + return ERR_AUTH_FAIL; + } +} + +static int oidc_auth_user(void *ctx, char *username, int username_size) +{ + oidc_ctx_st *ct = (oidc_ctx_st *) ctx; + + if (ct->token_verified) { + strlcpy(username, ct->username, username_size); + return 0; + } + return ERR_AUTH_FAIL; +} + +static int oidc_auth_pass(void *ctx, const char *pass, unsigned pass_len) +{ + return ERR_AUTH_FAIL; +} + +static int oidc_auth_msg(void *ctx, void *pool, passwd_msg_st * pst) +{ + pst->counter = 0; /* we support a single password */ + + /* use the default prompt */ + return 0; +} + +static void oidc_auth_deinit(void *ctx) +{ + talloc_free(ctx); +} + +const struct auth_mod_st oidc_auth_funcs = { + .type = AUTH_TYPE_OIDC, + .allows_retries = 1, + .vhost_init = oidc_vhost_init, + .vhost_deinit = oidc_vhost_deinit, + .auth_init = oidc_auth_init, + .auth_deinit = oidc_auth_deinit, + .auth_msg = oidc_auth_msg, + .auth_pass = oidc_auth_pass, + .auth_user = oidc_auth_user, + .auth_group = NULL, + .group_list = NULL +}; + +// Key management +typedef struct oidc_json_parser_context { + void *pool; + char *buffer; + size_t length; + size_t offset; +} oidc_json_parser_context; + +// Callback from CURL for each block as it is downloaded +static size_t oidc_json_parser_context_callback(char *ptr, size_t size, + size_t nmemb, void *userdata) +{ + oidc_json_parser_context *context = + (oidc_json_parser_context *) userdata; + size_t new_offset = context->offset + nmemb; + + // Check for buffer overflow + if (new_offset < nmemb) { + return 0; + } + + if (context->offset + nmemb > context->length) { + size_t new_size = (nmemb + context->length) * 3 / 2; + void * new_buffer = talloc_realloc_size(context->pool, context->buffer, new_size); + if (new_buffer) { + context->buffer = new_buffer; + context->length = new_size; + } else { + return 0; + } + } + + memcpy(context->buffer + context->offset, ptr, nmemb); + context->offset = new_offset; + + return nmemb; +} + +// Download a JSON file from the provided URI and return it in a jansson object +static json_t *oidc_fetch_json_from_uri(void * pool, const char *uri) +{ + oidc_json_parser_context context = { pool, NULL, 0, 0 }; + json_t *json = NULL; + json_error_t err; + CURL *curl = NULL; + CURLcode res; + + context.length = 4096; + context.buffer = talloc_size(context.pool, context.length); + + if (context.buffer == NULL) { + goto cleanup; + } + + curl = curl_easy_init(); + if (!curl) { + syslog(LOG_AUTH, + "ocserv-oidc: failed to download JSON document: URI %s\n", + uri); + goto cleanup; + } + + res = curl_easy_setopt(curl, CURLOPT_URL, uri); + if (res != CURLE_OK) { + syslog(LOG_AUTH, + "ocserv-oidc: failed to download JSON document: URI %s, CURLcode %d\n", + uri, res); + goto cleanup; + } + + res = + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, + oidc_json_parser_context_callback); + if (res != CURLE_OK) { + syslog(LOG_AUTH, + "ocserv-oidc: failed to download JSON document: URI %s, CURLcode %d\n", + uri, res); + goto cleanup; + } + + res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context); + if (res != CURLE_OK) { + syslog(LOG_AUTH, + "ocserv-oidc: failed to download JSON document: URI %s, CURLcode %d\n", + uri, res); + goto cleanup; + } + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + syslog(LOG_AUTH, + "ocserv-oidc: failed to download JSON document: URI %s, CURLcode %d\n", + uri, res); + goto cleanup; + } + + json = json_loadb(context.buffer, context.offset, 0, &err); + if (!json) { + syslog(LOG_AUTH, + "ocserv-oidc: failed to parse JSON document: URI %s\n", + uri); + goto cleanup; + } + + cleanup: + if (context.buffer) { + talloc_free(context.buffer); + } + + if (curl) { + curl_easy_cleanup(curl); + } + + return json; +} + +// Download and parse the JWT keys for this virtual server context +static bool oidc_fetch_oidc_keys(void * pool, oidc_vctx_st * vctx) +{ + bool result = false; + json_t *jwks = NULL; + json_t *openid_configuration_url = + json_object_get(vctx->config, "openid_configuration_url"); + + if (!openid_configuration_url) { + syslog(LOG_AUTH, + "ocserv-oidc: openid_configuration_url missing from config\n"); + goto cleanup; + } + + json_t *oidc_config = + oidc_fetch_json_from_uri(pool, + json_string_value + (openid_configuration_url)); + + if (!oidc_config) { + syslog(LOG_AUTH, + "ocserv-oidc: Unable to fetch config doc from %s\n", json_string_value(openid_configuration_url)); + goto cleanup; + } + + json_t *jwks_uri = json_object_get(oidc_config, "jwks_uri"); + if (!jwks_uri || !json_string_value(jwks_uri)) { + syslog(LOG_AUTH, + "ocserv-oidc: jwks_uri missing from config doc\n"); + goto cleanup; + } + + jwks = oidc_fetch_json_from_uri(pool, json_string_value(jwks_uri)); + if (!jwks) { + syslog(LOG_AUTH, + "ocserv-oidc: failed to fetch keys from jwks_uri %s\n", + json_string_value(jwks_uri)); + goto cleanup; + } + + if (vctx->jwks) { + json_decref(vctx->jwks); + } + + vctx->jwks = jwks; + jwks = NULL; + result = true; + + cleanup: + if (oidc_config) { + json_decref(oidc_config); + } + + if (jwks) { + json_decref(oidc_config); + } + return result; +} + +static bool oidc_verify_lifetime(json_t * token_claims) +{ + bool result = false; + + // Get the time bounds of the token + json_t *token_nbf = json_object_get(token_claims, "nbf"); + json_t *token_iat = json_object_get(token_claims, "iat"); + json_t *token_exp = json_object_get(token_claims, "exp"); + time_t current_time = time(NULL); + + if (!token_nbf || !json_integer_value(token_nbf)) { + syslog(LOG_AUTH, "ocserv-oidc: Token missing 'nbf' claim\n"); + goto cleanup; + } + + if (!token_exp || !json_integer_value(token_exp)) { + syslog(LOG_AUTH, "ocserv-oidc: Token missing 'exp' claim\n"); + goto cleanup; + } + + if (!token_iat || !json_integer_value(token_iat)) { + syslog(LOG_AUTH, "ocserv-oidc: Token missing 'iat' claim\n"); + goto cleanup; + } + + // Check to ensure the token is within it's validity + if (json_integer_value(token_nbf) > current_time + || json_integer_value(token_exp) < current_time) { + syslog(LOG_AUTH, + "ocserv-oidc: Token not within validity period NBF: %lld EXP: %lld Current: %ld\n", + json_integer_value(token_nbf), + json_integer_value(token_exp), current_time); + goto cleanup; + } + + result = true; + + cleanup: + return result; +} + +static bool oidc_verify_required_claims(json_t * required_claims, + json_t * token_claims) +{ + bool result = false; + + const char *required_claim_name; + json_t *required_claim_value; + json_t *token_claim_value; + + // Ensure all the required claims are present in the token + json_object_foreach(required_claims, required_claim_name, + required_claim_value) { + token_claim_value = + json_object_get(token_claims, required_claim_name); + if (!json_equal(required_claim_value, token_claim_value)) { + syslog(LOG_AUTH, + "ocserv-oidc: Required claim not met. Claim: %s Expected Value: %s\n", + required_claim_name, + json_string_value(required_claim_value)); + goto cleanup; + } + } + + result = true; + + cleanup: + return result; +} + +static bool oidc_map_user_name(json_t * user_name_claim, + json_t * token_claims, + char user_name[MAX_USERNAME_SIZE]) +{ + bool result = false; + + // Pull the user name from the token + json_t *token_user_name_claim = + json_object_get(token_claims, json_string_value(user_name_claim)); + if (!token_user_name_claim || !json_string_value(token_user_name_claim)) { + syslog(LOG_AUTH, "ocserv-oidc: Token missing '%s' claim\n", + json_string_value(user_name_claim)); + goto cleanup; + } + + strlcpy(user_name, json_string_value(token_user_name_claim), + MAX_USERNAME_SIZE); + result = true; + + cleanup: + return result; +} + +static json_t *oidc_extract_claims(cjose_jws_t * jws) +{ + cjose_err err; + json_error_t json_err; + uint8_t *plain_text = NULL; + size_t plain_text_size = 0; + json_t *token_claims = NULL; + + // Extract the claim portion from the token + if (!cjose_jws_get_plaintext(jws, &plain_text, &plain_text_size, &err)) { + syslog(LOG_AUTH, + "ocserv-oidc: Failed to get plain text from token\n"); + goto cleanup; + } + + // Parse the claim JSON + token_claims = + json_loadb((char *)plain_text, plain_text_size, 0, &json_err); + if (!token_claims) { + syslog(LOG_AUTH, + "ocserv-oidc: Failed to get claims from token\n"); + goto cleanup; + } + + cleanup: + return token_claims; +} + +static bool oidc_verify_singature(oidc_vctx_st * vctx, cjose_jws_t * jws) +{ + bool result = false; + + cjose_err err; + cjose_jwk_t *jwk = NULL; + json_t *token_header; + json_t *token_kid; + json_t *token_typ; + json_t *array; + size_t index; + json_t *value; + + if (vctx->jwks == NULL) { + syslog(LOG_AUTH, "ocserv-oidc: JWK keys not available\n"); + goto cleanup; + } + + array = json_object_get(vctx->jwks, "keys"); + if (array == NULL) { + syslog(LOG_AUTH, "ocserv-oidc: JWK keys malformed\n"); + goto cleanup; + } + + // Get the token header + token_header = cjose_jws_get_protected(jws); + if (token_header == NULL) { + syslog(LOG_AUTH, + "ocserv-oidc: Token malformed - no header\n"); + goto cleanup; + } + + // Get the kid of the key used to sign this token + token_kid = json_object_get(token_header, "kid"); + if (token_kid == NULL || !json_string_value(token_kid)) { + syslog(LOG_AUTH, "ocserv-oidc: Token malformed - no kid\n"); + goto cleanup; + } + + token_typ = json_object_get(token_header, "typ"); + if (token_typ == NULL || !json_string_value(token_typ) || strcmp(json_string_value(token_typ), "JWT")) { + syslog(LOG_AUTH, "ocserv-oidc: Token malformed - wrong typ claim\n"); + goto cleanup; + } + + // Find the signing key in the keys collection + json_array_foreach(array, index, value) { + json_t *key_kid = json_object_get(value, "kid"); + if (json_equal(key_kid, token_kid)) { + jwk = cjose_jwk_import_json(value, &err); + break; + } + } + + if (jwk == NULL) { + syslog(LOG_AUTH, "ocserv-oidc: JWK with kid=%s not found\n", + json_string_value(token_kid)); + goto cleanup; + } + + if (!cjose_jws_verify(jws, jwk, &err)) { + syslog(LOG_AUTH, "ocserv-oidc: Token failed validation %s\n", + err.message); + goto cleanup; + } + + result = true; + + cleanup: + return result; +} + +// Verify that the provided token is signed +static bool oidc_verify_token(oidc_vctx_st * vctx, const char *token, + size_t token_length, + char user_name[MAX_USERNAME_SIZE]) +{ + bool result = false; + cjose_err err; + cjose_jws_t *jws = NULL; + json_t *token_claims = NULL; + + jws = cjose_jws_import(token, token_length, &err); + if (jws == NULL) { + syslog(LOG_AUTH, "ocserv-oidc: Token malformed - %s\n", + err.message); + goto cleanup; + } + + if (!oidc_verify_singature(vctx, jws)) { + syslog(LOG_AUTH, + "ocserv-oidc: Token signature validation failed\n"); + goto cleanup; + } + + token_claims = oidc_extract_claims(jws); + if (!token_claims) { + syslog(LOG_AUTH, + "ocserv-oidc: Unable to access token claims\n"); + goto cleanup; + } + + if (!oidc_verify_lifetime(token_claims)) { + syslog(LOG_AUTH, + "ocserv-oidc: Token lifetime validation failed\n"); + goto cleanup; + } + + if (!oidc_verify_required_claims + (json_object_get(vctx->config, "required_claims"), token_claims)) { + syslog(LOG_AUTH, + "ocserv-oidc: Token required claims validation failed\n"); + goto cleanup; + } + + if (!oidc_map_user_name + (json_object_get(vctx->config, "user_name_claim"), token_claims, + user_name)) { + syslog(LOG_AUTH, + "ocserv-oidc: Unable to map user name claim\n"); + goto cleanup; + } + + result = true; + + cleanup: + if (jws) { + cjose_jws_release(jws); + } + + if (token_claims) { + json_decref(token_claims); + } + + return result; +} + +#endif diff --git a/src/auth/openidconnect.h b/src/auth/openidconnect.h new file mode 100644 index 00000000..9962be6a --- /dev/null +++ b/src/auth/openidconnect.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 Microsoft Corporation + * + * Author: Alan Jowett + * + * This file is part of ocserv. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see + */ +#ifndef OPENIDCONNECT_H +#define OPENIDCONNECT_H + +#include + +extern const struct auth_mod_st oidc_auth_funcs; + +#endif diff --git a/src/common-config.h b/src/common-config.h index 2a08c2a5..d2bdbf30 100644 --- a/src/common-config.h +++ b/src/common-config.h @@ -67,6 +67,7 @@ void *gssapi_get_brackets_string(void *pool, struct perm_cfg_st *config, const c void *radius_get_brackets_string(void *pool, struct perm_cfg_st *config, const char *str); void *pam_get_brackets_string(void *pool, struct perm_cfg_st *config, const char *str); void *plain_get_brackets_string(void *pool, struct perm_cfg_st *config, const char *str); +void *oidc_get_brackets_string(void * pool, struct perm_cfg_st *config, const char *str); void parse_kkdcp_string(char *str, int *socktype, char **_port, char **_server, char **_path, char **_realm); diff --git a/src/config.c b/src/config.c index 371843bb..bbe8842c 100644 --- a/src/config.c +++ b/src/config.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -177,6 +178,9 @@ static auth_types_st avail_auth_types[] = #endif {NAME("plain"), &plain_auth_funcs, AUTH_TYPE_PLAIN, plain_get_brackets_string}, {NAME("certificate"), NULL, AUTH_TYPE_CERTIFICATE, NULL}, +#ifdef SUPPORT_OIDC_AUTH + {NAME("oidc"), &oidc_auth_funcs, AUTH_TYPE_OIDC, oidc_get_brackets_string}, +#endif }; static void figure_auth_funcs(void *pool, const char *vhostname, @@ -1574,6 +1578,9 @@ static void print_version(void) append("PKCS#11"); #ifdef ANYCONNECT_CLIENT_COMPAT append("AnyConnect"); +#endif +#ifdef SUPPORT_OIDC_AUTH + append("oidc_auth"); #endif fprintf(stderr, "\n"); diff --git a/src/sec-mod-auth.c b/src/sec-mod-auth.c index a8e9113e..995fb27c 100644 --- a/src/sec-mod-auth.c +++ b/src/sec-mod-auth.c @@ -836,8 +836,11 @@ int handle_sec_auth_init(int cfd, sec_mod_st *sec, const SecAuthInitMsg *req, pi if (req->user_agent != NULL) strlcpy(e->acct_info.user_agent, req->user_agent, sizeof(e->acct_info.user_agent)); - if (req->user_name != NULL) { - strlcpy(e->acct_info.username, req->user_name, sizeof(e->acct_info.username)); + // Real user name is retrieved after auth. + if (!(req->auth_type & CONFIDENTIAL_USER_NAME_AUTH_TYPES)) { + if (req->user_name != NULL) { + strlcpy(e->acct_info.username, req->user_name, sizeof(e->acct_info.username)); + } } if (req->our_ip != NULL) { diff --git a/src/subconfig.c b/src/subconfig.c index fa9175ce..70b2191a 100644 --- a/src/subconfig.c +++ b/src/subconfig.c @@ -332,3 +332,22 @@ void *plain_get_brackets_string(void *pool, struct perm_cfg_st *config, const ch return additional; } + + +void *oidc_get_brackets_string(void * pool, struct perm_cfg_st *config, const char *str) +{ + subcfg_val_st vals[MAX_SUBOPTIONS]; + char * additional = NULL; + + unsigned vals_size, i; + + vals_size = expand_brackets_string(pool, str, vals); + + for (i = 0; i < vals_size; i ++) { + if (c_strcasecmp(vals[i].name, "config") == 0) { + additional = talloc_strdup(pool, vals[i].value); + } + } + + return additional; +} diff --git a/src/vpn.h b/src/vpn.h index cd53ce07..b55036aa 100644 --- a/src/vpn.h +++ b/src/vpn.h @@ -135,9 +135,11 @@ extern int syslog_open; #define AUTH_TYPE_CERTIFICATE (1<<3) #define AUTH_TYPE_RADIUS (1<<5 | AUTH_TYPE_USERNAME_PASS) #define AUTH_TYPE_GSSAPI (1<<6) +#define AUTH_TYPE_OIDC (1<<7) -#define ALL_AUTH_TYPES ((AUTH_TYPE_PAM|AUTH_TYPE_PLAIN|AUTH_TYPE_CERTIFICATE|AUTH_TYPE_RADIUS|AUTH_TYPE_GSSAPI) & (~AUTH_TYPE_USERNAME_PASS)) +#define ALL_AUTH_TYPES ((AUTH_TYPE_PAM|AUTH_TYPE_PLAIN|AUTH_TYPE_CERTIFICATE|AUTH_TYPE_RADIUS|AUTH_TYPE_GSSAPI|AUTH_TYPE_OIDC) & (~AUTH_TYPE_USERNAME_PASS)) #define VIRTUAL_AUTH_TYPES (AUTH_TYPE_USERNAME_PASS) +#define CONFIDENTIAL_USER_NAME_AUTH_TYPES (AUTH_TYPE_GSSAPI | AUTH_TYPE_OIDC) #define ACCT_TYPE_PAM (1<<1) #define ACCT_TYPE_RADIUS (1<<2) @@ -193,6 +195,7 @@ typedef struct auth_struct_st { unsigned type; const struct auth_mod_st *amod; void *auth_ctx; + void *dl_ctx; bool enabled; } auth_struct_st; diff --git a/src/worker-auth.c b/src/worker-auth.c index 24ccb4ef..09bc141e 100644 --- a/src/worker-auth.c +++ b/src/worker-auth.c @@ -102,12 +102,20 @@ static const char login_msg_user[] = #define OCV3_LOGIN_MSG_START _OCV3_LOGIN_MSG_START("main") #define OCV3_PASSWD_MSG_START _OCV3_LOGIN_MSG_START("passwd") +#ifdef SUPPORT_OIDC_AUTH +#define HTTP_AUTH_OIDC_PREFIX "Bearer" +#endif + static const char ocv3_login_msg_end[] = "\n"; static int get_cert_info(worker_st * ws); static int basic_auth_handler(worker_st * ws, unsigned http_ver, const char *msg); +#ifdef SUPPORT_OIDC_AUTH +static int oidc_auth_handler(worker_st * ws, unsigned http_ver); +#endif + int ws_switch_auth_to(struct worker_st *ws, unsigned auth) { unsigned i; @@ -1330,6 +1338,49 @@ int basic_auth_handler(worker_st * ws, unsigned http_ver, const char *msg) return ret; } +#ifdef SUPPORT_OIDC_AUTH +static +int oidc_auth_handler(worker_st * ws, unsigned http_ver) +{ + int ret; + + oclog(ws, LOG_HTTP_DEBUG, "HTTP sending: 401 Unauthorized"); + cstp_cork(ws); + ret = cstp_printf(ws, "HTTP/1.%u 401 Unauthorized\r\n", http_ver); + if (ret < 0) + return -1; + + oclog(ws, LOG_HTTP_DEBUG, "HTTP sending: WWW-Authenticate: %s", HTTP_AUTH_OIDC_PREFIX); + ret = cstp_printf(ws, "WWW-Authenticate: %s\r\n", HTTP_AUTH_OIDC_PREFIX); + + if (ret < 0) + return -1; + + ret = cstp_puts(ws, "Content-Length: 0\r\n"); + if (ret < 0) { + ret = -1; + goto cleanup; + } + + ret = cstp_puts(ws, "\r\n"); + if (ret < 0) { + ret = -1; + goto cleanup; + } + + ret = cstp_uncork(ws); + if (ret < 0) { + ret = -1; + goto cleanup; + } + + ret = 0; + + cleanup: + return ret; +} +#endif + #define USERNAME_FIELD "username" #define GROUPNAME_FIELD "group%5flist" #define GROUPNAME_FIELD2 "group_list" @@ -1387,6 +1438,21 @@ int post_auth_handler(worker_st * ws, unsigned http_ver) } talloc_free(groupname); +#ifdef SUPPORT_OIDC_AUTH + if (ws->selected_auth->type & AUTH_TYPE_OIDC) { + if (req->authorization == NULL || req->authorization_size == 0) + return oidc_auth_handler(ws, http_ver); + + if ((req->authorization_size > (sizeof(HTTP_AUTH_OIDC_PREFIX) - 1)) && strncasecmp(req->authorization, HTTP_AUTH_OIDC_PREFIX, sizeof(HTTP_AUTH_OIDC_PREFIX) - 1) == 0) { + ireq.auth_type |= AUTH_TYPE_OIDC; + ireq.user_name = req->authorization + sizeof(HTTP_AUTH_OIDC_PREFIX); + } else { + oclog(ws, LOG_HTTP_DEBUG, "Invalid authorization data: %.*s", req->authorization_size, req->authorization); + goto auth_fail; + } + } +#endif + if (ws->selected_auth->type & AUTH_TYPE_GSSAPI) { if (req->authorization == NULL || req->authorization_size == 0) return basic_auth_handler(ws, http_ver, NULL); @@ -1484,7 +1550,6 @@ int post_auth_handler(worker_st * ws, unsigned http_ver) SecAuthContMsg areq = SEC_AUTH_CONT_MSG__INIT; areq.ip = ws->remote_ip_str; - if (ws->selected_auth->type & AUTH_TYPE_GSSAPI) { if (req->authorization == NULL || req->authorization_size <= 10) { if (req->authorization != NULL) @@ -1555,6 +1620,7 @@ int post_auth_handler(worker_st * ws, unsigned http_ver) } if (ret == ERR_AUTH_CONTINUE) { + oclog(ws, LOG_DEBUG, "continuing authentication for '%s'", ws->username); ws->auth_state = S_AUTH_REQ; diff --git a/tests/Makefile.am b/tests/Makefile.am index ea435777..0e0fe0ae 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -151,6 +151,7 @@ human_addr_CPPFLAGS = $(AM_CPPFLAGS) human_addr_SOURCES = human_addr.c human_addr_LDADD = $(LDADD) + valid_hostname_LDADD = $(LDADD) port_parsing_LDADD = $(LDADD) @@ -159,8 +160,16 @@ check_PROGRAMS = str-test str-test2 ipv4-prefix ipv6-prefix kkdcp-parsing json-e port-parsing human_addr valid-hostname url-escape html-escape cstp-recv \ proxyproto-v1 +gen_oidc_test_data_CPPFLAGS = $(AM_CPPFLAGS) +gen_oidc_test_data_SOURCES = generate_oidc_test_data.c +gen_oidc_test_data_LDADD = $(LDADD) $(CJOSE_LIBS) $(JANSSON_LIBS) -TESTS = $(dist_check_SCRIPTS) $(check_PROGRAMS) $(xfail_scripts) +if ENABLE_OIDC_AUTH_TESTS +check_PROGRAMS += gen_oidc_test_data +dist_check_SCRIPTS += test-oidc +endif + +TESTS = $(check_PROGRAMS) $(dist_check_SCRIPTS) $(xfail_scripts) XFAIL_TESTS = $(xfail_scripts) diff --git a/tests/config-auth.xml b/tests/config-auth.xml new file mode 100644 index 00000000..3273e5c8 --- /dev/null +++ b/tests/config-auth.xml @@ -0,0 +1,7 @@ + + + + v5.01 + + + diff --git a/tests/data/test-oidc-auth.config b/tests/data/test-oidc-auth.config new file mode 100644 index 00000000..a0137ce6 --- /dev/null +++ b/tests/data/test-oidc-auth.config @@ -0,0 +1,198 @@ +# User authentication method. Could be set multiple times and in that case +# all should succeed. +# Options: certificate, pam. +auth = "oidc[config=@SRCDIR@/data/oidc.json]" + +isolate-workers = false + +# A banner to be displayed on clients +#banner = "Welcome" + +# Use listen-host to limit to specific IPs or to the IPs of a provided hostname. +#listen-host = [IP|HOSTNAME] + +use-dbus = no + +# Limit the number of clients. Unset or set to zero for unlimited. +#max-clients = 1024 +max-clients = 16 + +# Limit the number of client connections to one every X milliseconds +# (X is the provided value). Set to zero for no limit. +#rate-limit-ms = 100 + +# Limit the number of identical clients (i.e., users connecting multiple times) +# Unset or set to zero for unlimited. +max-same-clients = 2 + +# TCP and UDP port number +tcp-port = @PORT@ +udp-port = @PORT@ + +# Keepalive in seconds +keepalive = 32400 + +# Dead peer detection in seconds +dpd = 440 + +# MTU discovery (DPD must be enabled) +try-mtu-discovery = false + +# The key and the certificates of the server +# The key may be a file, or any URL supported by GnuTLS (e.g., +# tpmkey:uuid=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx;storage=user +# or pkcs11:object=my-vpn-key;object-type=private) +# +# There may be multiple certificate and key pairs and each key +# should correspond to the preceding certificate. +server-cert = @SRCDIR@/certs/server-cert.pem +server-key = @SRCDIR@/certs/server-key.pem + +# Diffie-Hellman parameters. Only needed if you require support +# for the DHE ciphersuites (by default this server supports ECDHE). +# Can be generated using: +# certtool --generate-dh-params --outfile /path/to/dh.pem +#dh-params = /path/to/dh.pem + +# If you have a certificate from a CA that provides an OCSP +# service you may provide a fresh OCSP status response within +# the TLS handshake. That will prevent the client from connecting +# independently on the OCSP server. +# You can update this response periodically using: +# ocsptool --ask --load-cert=your_cert --load-issuer=your_ca --outfile response +# Make sure that you replace the following file in an atomic way. +#ocsp-response = /path/to/ocsp.der + +# In case PKCS #11 or TPM keys are used the PINs should be available +# in files. The srk-pin-file is applicable to TPM keys only (It's the storage +# root key). +#pin-file = /path/to/pin.txt +#srk-pin-file = /path/to/srkpin.txt + +# The Certificate Authority that will be used +# to verify clients if certificate authentication +# is set. +#ca-cert = /path/to/ca.pem + +# The object identifier that will be used to read the user ID in the client certificate. +# The object identifier should be part of the certificate's DN +# Useful OIDs are: +# CN = 2.5.4.3, UID = 0.9.2342.19200300.100.1.1 +#cert-user-oid = 0.9.2342.19200300.100.1.1 + +# The object identifier that will be used to read the user group in the client +# certificate. The object identifier should be part of the certificate's DN +# Useful OIDs are: +# OU (organizational unit) = 2.5.4.11 +#cert-group-oid = 2.5.4.11 + +# A revocation list of ca-cert is set +#crl = /path/to/crl.pem + +# GnuTLS priority string +tls-priorities = "PERFORMANCE:%SERVER_PRECEDENCE:%COMPAT" + +# To enforce perfect forward secrecy (PFS) on the main channel. +#tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA" + +# The time (in seconds) that a client is allowed to stay connected prior +# to authentication +auth-timeout = 40 + +# The time (in seconds) that a client is not allowed to reconnect after +# a failed authentication attempt. +min-reauth-time = 20 + +max-ban-score = 9999999 + +# The time (in seconds) that all score kept for a client is reset. +ban-reset-time = 10 + +# In case you'd like to change the default points. +ban-points-wrong-password = 10 +ban-points-connection = 1 +ban-points-kkdcp = 1 + + +# Cookie timeout (in seconds) +# Once a client is authenticated he's provided a cookie with +# which he can reconnect. That cookie will be invalided if not +# used within this timeout value. On a user disconnection, that +# cookie will also be active for this time amount prior to be +# invalid. That should allow a reasonable amount of time for roaming +# between different networks. +cookie-timeout = 30 + +# Script to call when a client connects and obtains an IP +# Parameters are passed on the environment. +# REASON, USERNAME, GROUPNAME, HOSTNAME (the hostname selected by client), +# DEVICE, IP_REAL (the real IP of the client), IP_LOCAL (the local IP +# in the P-t-P connection), IP_REMOTE (the VPN IP of the client). REASON +# may be "connect" or "disconnect". +#connect-script = /usr/bin/myscript +#disconnect-script = /usr/bin/myscript + +# UTMP +use-utmp = true + +# PID file +pid-file = /var/run/ocserv.pid + +# The default server directory. Does not require any devices present. +#chroot-dir = /path/to/chroot + +# socket file used for IPC, will be appended with .PID +# It must be accessible within the chroot environment (if any) +socket-file = @OCCTL_SOCKET@ + +# The user the worker processes will be run as. It should be +# unique (no other services run as this user). +run-as-user = @USERNAME@ +run-as-group = @GROUP@ + +# Network settings + +device = vpns + +# The default domain to be advertised +default-domain = example.com + +ipv4-network = 192.168.1.0 +ipv4-netmask = 255.255.255.0 +# Use the keywork local to advertize the local P-t-P address as DNS server +dns = 192.168.1.1 + +# The NBNS server (if any) +#ipv4-nbns = 192.168.2.3 + +ipv6-network = fe80:: +ipv6-prefix = 16 +#ipv6-dns = + +# Prior to leasing any IP from the pool ping it to verify that +# it is not in use by another (unrelated to this server) host. +ping-leases = false + +# Leave empty to assign the default MTU of the device +# mtu = + +route = 192.168.1.0/255.255.255.0 +#route = 192.168.5.0/255.255.255.0 + +# +# The following options are for (experimental) AnyConnect client +# compatibility. They are only available if the server is built +# with --enable-anyconnect +# + +# Client profile xml. A sample file exists in doc/profile.xml. +# This file must be accessible from inside the worker's chroot. +# The profile is ignored by the openconnect client. +#user-profile = profile.xml + +# Unless set to false it is required for clients to present their +# certificate even if they are authenticating via a previously granted +# cookie. Legacy CISCO clients do not do that, and thus this option +# should be set for them. +#always-require-cert = false + diff --git a/tests/generate_oidc_test_data.c b/tests/generate_oidc_test_data.c new file mode 100644 index 00000000..9d2513cc --- /dev/null +++ b/tests/generate_oidc_test_data.c @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2020 Microsoft Corporation + * + * Author: Alan Jowett + * + * This file is part of ocserv. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see + */ + +#include +#include + +#include +#include +#include + +cjose_jwk_t *create_key(const char *kid) +{ + cjose_err err; + cjose_jwk_t *key = cjose_jwk_create_EC_random(CJOSE_JWK_EC_P_256, &err); + if (!key) { + return NULL; + } + + if (!cjose_jwk_set_kid(key, kid, strlen(kid), &err)) { + return NULL; + } + return key; +} + +json_t *create_oidc_config(const char *openid_configuration_url, + const char *user_name_claim, const char *audience, + const char *issuer) +{ + bool result = false; + json_t *config = json_object(); + json_t *required_claims = json_object(); + if (json_object_set_new + (config, "openid_configuration_url", + json_string(openid_configuration_url))) { + goto cleanup; + } + + if (json_object_set_new + (config, "user_name_claim", json_string(user_name_claim))) { + goto cleanup; + } + + if (json_object_set_new(required_claims, "aud", json_string(audience))) { + goto cleanup; + } + + if (json_object_set_new(required_claims, "iss", json_string(issuer))) { + goto cleanup; + } + + if (json_object_set_new(config, "required_claims", required_claims)) { + goto cleanup; + } + + required_claims = NULL; + + result = true; + + cleanup: + if (!result && config) { + json_decref(config); + config = NULL; + } + + return config; +} + +json_t *create_openid_configuration(char *key_url) +{ + json_t *config = json_object(); + if (json_object_set_new(config, "jwks_uri", json_string(key_url))) { + json_decref(config); + return NULL; + } + return config; +} + +json_t *create_keys(cjose_jwk_t * key) +{ + cjose_err err; + json_t *keys_json = json_object(); + json_t *keys_array = json_array(); + json_t *key_json; + + const char *key_str = cjose_jwk_to_json(key, false, &err); + key_json = json_loads(key_str, 0, NULL); + json_array_append_new(keys_array, key_json); + json_object_set_new(keys_json, "keys", keys_array); + return keys_json; +} + +json_t *create_header(const char *typ, const char *alg, const char *kid) +{ + json_t *header_json = json_object(); + if (typ) { + json_object_set_new(header_json, "typ", json_string(typ)); + } + if (alg) { + json_object_set_new(header_json, "alg", json_string(alg)); + } + if (kid) { + json_object_set_new(header_json, "kid", json_string(kid)); + } + return header_json; +} + +json_t *create_claims(const char *audience, const char *issuer, + json_int_t issued_at, json_int_t not_before, + json_int_t expires, const char *preferred_user_name) +{ + json_t *claims_json = json_object(); + if (audience) { + json_object_set_new(claims_json, "aud", json_string(audience)); + } + if (issuer) { + json_object_set_new(claims_json, "iss", json_string(issuer)); + } + if (issued_at) { + json_object_set_new(claims_json, "iat", + json_integer(issued_at)); + } + if (not_before) { + json_object_set_new(claims_json, "nbf", + json_integer(not_before)); + } + if (expires) { + json_object_set_new(claims_json, "exp", json_integer(expires)); + } + if (preferred_user_name) { + json_object_set_new(claims_json, "preferred_username", + json_string(preferred_user_name)); + } + return claims_json; +} + +cjose_jws_t *create_jws(cjose_jwk_t * key, json_t * header, json_t * claims) +{ + cjose_err err; + char *claims_str = json_dumps(claims, 0); + cjose_jws_t *jws = + cjose_jws_sign(key, header, (const uint8_t *)claims_str, + strlen(claims_str), &err); + free(claims_str); + return jws; +} + +bool write_jws_to_file(cjose_jws_t * jws, const char *file) +{ + cjose_err err; + const char *jws_str; + FILE *f = fopen(file, "w"); + if (!cjose_jws_export(jws, &jws_str, &err)) { + fclose(f); + return false; + } + + fprintf(f, "%s", jws_str); + fclose(f); + return true; +} + +void generate_token(const char *output_folder, const char *token_name, + cjose_jwk_t * key, const char *typ, const char *alg, + const char *kid, const char *audience, const char *issuer, + const char *user_name, json_int_t issued_at, + json_int_t not_before, json_int_t expires) +{ + char token_file[1024]; + snprintf(token_file, sizeof(token_file), "%s/%s.token", output_folder, + token_name); + json_t *header = create_header(typ, alg, kid); + json_t *claims = + create_claims(audience, issuer, issued_at, not_before, expires, + user_name); + cjose_jws_t *jws = create_jws(key, header, claims); + write_jws_to_file(jws, token_file); + + cjose_jws_release(jws); + json_decref(header); + json_decref(claims); +} + +void generate_config_files(const char *output_folder, cjose_jwk_t * key, + const char *expected_audience, + const char *expected_issuer, + const char *user_name_claim) +{ + char oidc_config_file[1024]; + char openid_configuration_file[1024]; + char keys_file[1024]; + char openid_configuration_uri[1024]; + char keys_uri[1024]; + int retval; + retval = + snprintf(oidc_config_file, sizeof(oidc_config_file), "%s/oidc.json", + output_folder); + + retval = + snprintf(openid_configuration_file, + sizeof(openid_configuration_file), + "%s/openid-configuration.json", output_folder); + if (retval < 0 || retval > sizeof(openid_configuration_file)) { + exit(1); + } + + retval = + snprintf(keys_file, sizeof(keys_file), "%s/keys.json", + output_folder); + if (retval < 0 || retval > sizeof(openid_configuration_file)) { + exit(1); + } + retval = + snprintf(openid_configuration_uri, sizeof(openid_configuration_uri), + "file://localhost%s", openid_configuration_file); + if (retval < 0 || retval > sizeof(openid_configuration_file)) { + exit(1); + } + + retval = + snprintf(keys_uri, sizeof(keys_uri), "file://localhost%s", + keys_file); + if (retval < 0 || retval > sizeof(openid_configuration_file)) { + exit(1); + } + + json_t *oidc_config = + create_oidc_config(openid_configuration_uri, "preferred_username", + "SomeAudience", "SomeIssuer"); + json_t *openid_configuration = create_openid_configuration(keys_uri); + json_t *keys = create_keys(key); + + json_dump_file(oidc_config, oidc_config_file, 0); + json_dump_file(openid_configuration, openid_configuration_file, 0); + json_dump_file(keys, keys_file, 0); + + json_decref(oidc_config); + json_decref(openid_configuration); + json_decref(keys); +} + +int main(int argc, char **argv) +{ + char working_directory[1024]; + const char audience[] = "SomeAudience"; + const char issuer[] = "SomeIssuer"; + const char user_name_claim[] = "preferred_user_name"; + const char kid[] = "My Fake Key"; + const char user_name[] = "SomeUser"; + const char typ[] = "JWT"; + const char alg[] = "ES256"; + time_t now = time(NULL); + + if (!getcwd(working_directory, sizeof(working_directory))) { + return 1; + } + strncat(working_directory, "/data", sizeof(working_directory)); + + cjose_jwk_t *key = create_key("My Fake Key"); + + generate_config_files(working_directory, key, audience, issuer, + user_name_claim); + + generate_token(working_directory, "success_good", key, typ, alg, kid, + audience, issuer, user_name, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_expired", key, typ, alg, kid, + audience, issuer, user_name, now - 7260, now - 7260, + now - 3600); + generate_token(working_directory, "fail_bad_typ", key, "FOO", alg, kid, + audience, issuer, user_name, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_bad_alg", key, typ, "FOO", kid, + audience, issuer, user_name, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_wrong_kid", key, typ, alg, + "FOO", audience, issuer, user_name, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_wrong_aud", key, typ, alg, kid, + "FOO", issuer, user_name, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_wrong_iss", key, typ, alg, kid, + audience, "FOO", user_name, now - 60, now - 60, + now + 3600); + + generate_token(working_directory, "fail_missing_aud", key, typ, alg, + kid, NULL, issuer, user_name, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_missing_iss", key, typ, alg, + kid, audience, NULL, user_name, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_missing_user", key, typ, alg, + kid, audience, issuer, NULL, now - 60, now - 60, + now + 3600); + generate_token(working_directory, "fail_missing_iat", key, typ, alg, + kid, audience, issuer, user_name, 0, now - 60, + now + 3600); + generate_token(working_directory, "fail_missing_nbf", key, typ, alg, + kid, audience, issuer, user_name, now - 60, 0, + now + 3600); + generate_token(working_directory, "fail_missing_exp", key, typ, alg, + kid, audience, issuer, user_name, now - 60, now - 60, 0); + return 0; +} diff --git a/tests/test-oidc b/tests/test-oidc new file mode 100755 index 00000000..2a810630 --- /dev/null +++ b/tests/test-oidc @@ -0,0 +1,55 @@ +#!/bin/sh +# +# Copyright (C) 2020 Microsoft Corporation +# +# This file is part of ocserv. +# +# ocserv is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# ocserv is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GnuTLS; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +SERV="${SERV:-../src/ocserv}" +srcdir=${srcdir:-.} +NO_NEED_ROOT=1 +PORT=4503 +PIDFILE=ocserv-pid.$$.tmp +OCCTL_SOCKET=./occtl-oidc-$$.socket + +. `dirname $0`/common.sh + +echo "Testing local backend with oidc token auth... " + +update_config test-oidc-auth.config +launch_sr_server -d 1 -p ${PIDFILE} -f -c ${CONFIG} & PID=$! +wait_server $PID + +for token in data/success_*; do + http_result=$(LD_PRELOAD=libsocket_wrapper.so curl --insecure https://$ADDRESS:$PORT --request POST --data config-auth.xml --header "Authorization:Bearer=`cat $token`" --output /dev/null --write-out "%{http_code}") + if [ "$http_result" != "200" ]; then + fail $PID "Token incorrectly rejected returned $http_result" + fi +done +for token in data/fail_*; do + http_result=$(LD_PRELOAD=libsocket_wrapper.so curl --insecure https://$ADDRESS:$PORT --request POST --data config-auth.xml --header "Authorization:Bearer=`cat $token`" --output /dev/null --silent --write-out "%{http_code}") + if [ "$http_result" != "401" ]; then + fail $PID "Token incorrectly accepted returned $http_result" + fi +done + +if ! test -f ${PIDFILE};then + fail $PID "Could not find pid file ${PIDFILE}" +fi + +cleanup + +exit 0