diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9c5936c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/aclocal.m4 +/.cproject +/.project +/config.log +/config.status +/configure +/Makefile +/discover +/metadata diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..9697d472 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,2 @@ +3/27/2014 +- initial import named mod_auth_openidc diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile.in b/Makefile.in new file mode 100644 index 00000000..cb77abbc --- /dev/null +++ b/Makefile.in @@ -0,0 +1,70 @@ + +# Source files. mod_auth_openidc.c must be the first file. +SRC=src/mod_auth_openidc.c \ + src/cache/file.c \ + src/cache/memcache.c \ + src/cache/shm.c \ + src/oauth.c \ + src/proto.c \ + src/crypto.c \ + src/config.c \ + src/util.c \ + src/authz.c \ + src/session.c \ + src/metadata.c \ + src/apr_json_decode.c + +# Files to include when making a .tar.gz-file for distribution +DISTFILES=$(SRC) \ + src/mod_auth_openidc.h \ + src/apr_json.h \ + src/cache/cache.h \ + configure \ + configure.ac \ + Makefile.in \ + autogen.sh \ + README \ + LICENSE.txt \ + ChangeLog + + +all: src/mod_auth_openidc.la + +src/mod_auth_openidc.la: $(SRC) src/mod_auth_openidc.h src/apr_json.h + @APXS2@ -Wc,"@OPENSSL_CFLAGS@ @CURL_CFLAGS@" -Wl,"@OPENSSL_LIBS@ @CURL_LIBS@" -Wc,-Wall -Wc,-g -c $(SRC) + + +# Building configure (for distribution) +configure: configure.ac + ./autogen.sh + +@NAMEVER@.tar.gz: $(DISTFILES) + tar -c --transform="s#^#@NAMEVER@/#" -vzf $@ $(DISTFILES) + + +.PHONY: install +install: src/mod_auth_openidc.la + @APXS2@ -i -n mod_auth_openidc src/mod_auth_openidc.la + +.PHONY: distfile +distfile: @NAMEVER@.tar.gz + +.PHONY: clean +clean: + rm -f src/mod_auth_openidc.la + rm -f src/*.o src/cache/*.o + rm -f src/*.lo src/cache/*.lo + rm -f src/*.slo src/cache/*.slo + rm -rf src/cache/.libs/ + rm -rf src/.libs/ + +.PHONY: distclean +distclean: clean + rm -f Makefile config.log config.status @NAMEVER@.tar.gz *~ \ + build-stamp config.guess config.sub + rm -rf debian/mod-auth_openidc + rm -f debian/files + +.PHONY: fullclean +fullclean: distclean + rm -f configure aclocal.m4 diff --git a/README b/README new file mode 100644 index 00000000..3b5f9d1e --- /dev/null +++ b/README @@ -0,0 +1,150 @@ +mod_auth_openidc is an Apache authentication/authorization module that allows an Apache +server to operate as an OpenID Connect Relying Party. + +It requires users to authenticate at an external OpenID Connect Identity Provider +using the OpenID Connect Basic Client or Implicit Client profile. + +It sets the REMOTE_USER variable to the id_token sub claim, other id_token claims +are passed in HTTP headers, together with those (optionally) obtained from the user info endpoint + +It allows for authorization rules (based on Requires primitive) that be matched against the +set of claims provided in the id_token/userinfo. + +It supports multiple OpenID Connect Providers by reading provider metadata files from a +metadata directory (set by OIDCMetadataDir). The provider metadata files must follow the +naming convention ".provider" (with the "https://" prefix stripped). + +It supports OpenID Connect Dynamic Client Registration by setting OIDCMetadataDir (which needs +to be writable for the Apache process in this case), a provider metadata file exists but for the +specified issuer, it contains a registration_endpoint setting, but no matching valid client +metadata (cq.".client") file can be found in the metadata directory. + +It supports OpenID Provider Discovery through account names (cq. @) by setting +OIDCMetadataDir (which needs to be writable for the Apache process in this case), and +calling (HTTP GET) the Redirect URI with a "oidc_acct" parameter that contains the user account +name (see sample config for multiple OPs and an external Discovery page below). + +Additionally it can operate as an OAuth 2.0 Resource Server to a PingFederate OAuth 2.0 +Authorization Server, cq. validate Bearer access_tokens against PingFederate. +In that case it sets the REMOTE_USER variable to the "Username" claim and matches the claims +in the intro-spected access_token against the Requires primitive. + +It implements server-side caching across different Apache processes through one of the following options: + a) file storage: in a temp directory - possibly a shared file system across multiple Apache servers + b) shared memory: shared across a single logical Apache server running in multiple Apache processes (mpm_prefork) on the same machine + c) memcache: shared across multiple Apache processes and servers, possibly across different memcache servers living on different machines + + + +Example config for using Google Apps as your OpenID Connect Provider: +(running on localhost and https://localhost/example/redirect_uri/ registered as redirect_uri for the client) +========================================================== +OIDCProviderIssuer accounts.google.com +OIDCProviderAuthorizationEndpoint https://accounts.google.com/o/oauth2/auth?approval_prompt=force&[hd=] +OIDCProviderTokenEndpoint https://accounts.google.com/o/oauth2/token +OIDCProviderTokenEndpointAuth client_secret_post +OIDCProviderUserInfoEndpoint https://www.googleapis.com/plus/v1/people/me/openIdConnect +OIDCProviderJwksUri https://www.googleapis.com/oauth2/v2/certs + +OIDCClientID +OIDCClientSecret + +OIDCScope "openid email profile" +OIDCRedirectURI https://localhost/example/redirect_uri/ +OIDCCryptoPassphrase + + + Authtype openid-connect + require valid-user + +========================================================== + + + +Another example using multiple OpenID Connect providers, which triggers OP discovery first: + +OIDCMetadataDir points to a directory that contains files that contain per-provider configuration +data. For each provider, there are 2 types of files in the directory: + .provider: +contains (standard) OpenID Connect Discovery OP JSON metadata where each name of the file is +the urlencoded issuer name of the OP that is decribed by the metadata in that file. + .client: +contains mod_auth_openidc specific JSON metadata (based on the OpenID Connect Client Registration +specification, with extensions) and the filename is the urlencoded issuer name of the OP that +this client is registered with. + +Sample client metdata for issuer https://localhost:9031, so client metadata filename is: +"https%3a%2f%2fmacbook%3a9031.client" +========================================================== +{ + "ssl_validate_server" : "Off", + "client_id" : "ac_oic_client", + "client_secret" : "abc123DEFghijklmnop4567rstuvwxyzZYXWUT8910SRQPOnmlijhoauthplaygroundapplication", + "scope" : "openid email profile", +} +========================================================== + +And the related mod_auth_openidc Apache config section: +========================================================== +OIDCMetadataDir /metadata + +OIDCRedirectURI https://localhost/example/redirect_uri/ +OIDCCryptoPassphrase + + + Authtype openid-connect + require valid-user + +========================================================== + +If you do not want to use the internal discovery page, you can have the user being redirected to +an external discovery page by setting "OIDCDiscoveryURL". That URL will be accessed with 2 parameters, +"oidc_callback" and "oidc_return" (both URLs). The "oidc_return" parameter needs to be returned to the +"oidc_callback" URL (again in the oidc_return parameter) together with an "oidc_provider" parameter that +contains the URL-encoded issuer value of the selected Provider, or a URL-encoded account name for OpenID +Connect Discovery purposes (aka. e-mail style identifier), or a domain name. + +Sample callback: + ${oidc_callback}?oidc_return=${oidc_return}&oidc_provider=[${issuer}|${domain}|${e-mail-style-account-name}] + + + +Another example config for using PingFederate as your OpenID OP and/or OAuth 2.0 Authorization +server, based on the OAuth 2.0 PlayGround 3.x default configuration and doing claims-based authorization. +(running on localhost and https://localhost/example/redirect_uri/ registerd as redirect_uri for the client "ac_oic_client") + +========================================================== +OIDCProviderIssuer https://macbook:9031 +OIDCProviderAuthorizationEndpoint https://macbook:9031/as/authorization.oauth2 +OIDCProviderTokenEndpoint https://macbook:9031/as/token.oauth2 +OIDCProviderTokenEndpointAuth client_secret_basic +OIDCProviderUserInfoEndpoint https://macbook:9031/idp/userinfo.openid +OIDCProviderJwksUri https://macbook:9031/pf/JWKS + +OIDCSSLValidateServer Off +OIDCClientID ac_oic_client +OIDCClientSecret abc123DEFghijklmnop4567rstuvwxyzZYXWUT8910SRQPOnmlijhoauthplaygroundapplication + +OIDCRedirectURI https://localhost/example/redirect_uri/ +OIDCCryptoPassphrase +OIDCScope "openid email profile" + +OIDCOAuthEndpoint https://macbook:9031/as/token.oauth2 +OIDCOAuthEndpointAuth client_secret_basic + +OIDCOAuthSSLValidateServer Off +OIDCOAuthClientID rs_client +OIDCOAuthClientSecret 2Federate + + + Authtype openid-connect + #require valid-user + require claim sub:joe + + + + Authtype oauth20 + #require valid-user + require claim Username:joe + +========================================================== diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 00000000..832703fc --- /dev/null +++ b/autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh +autoreconf --force --install +rm -rf autom4te.cache/ diff --git a/configure.ac b/configure.ac new file mode 100644 index 00000000..bb24f513 --- /dev/null +++ b/configure.ac @@ -0,0 +1,55 @@ +AC_INIT([mod_auth_openidc],[1.0],[hzandbelt@pingidentity.com]) + +AC_SUBST(NAMEVER, AC_PACKAGE_TARNAME()-AC_PACKAGE_VERSION()) + +# This section defines the --with-apxs2 option. +AC_ARG_WITH( + [apxs2], + [ --with-apxs2=PATH Full path to the apxs2 executable.], + [ + APXS2=${withval} + ],) + + +if test "x$APXS2" = "x"; then + # The user didn't specify the --with-apxs2-option. + + # Search for apxs2 in the specified directories + AC_PATH_PROG(APXS2, apxs2,, + /usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin) + + if test "x$APXS2" = "x"; then + # Didn't find apxs2 in any of the specified directories. + # Search for apxs instead. + AC_PATH_PROG(APXS2, apxs,, + /usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin) + fi + +fi + +# Test if $APXS2 exists and is an executable. +if test ! -x "$APXS2"; then + # $APXS2 isn't a executable file. + AC_MSG_ERROR([ +Could not find apxs2. Please specify the path to apxs2 +using the --with-apxs2=/full/path/to/apxs2 option. +The executable may also be named 'apxs'. +]) +fi + +# Replace any occurrences of @APXS2@ with the value of $APXS2 in the Makefile. +AC_SUBST(APXS2) + +# We need the curl library for HTTP callouts. +PKG_CHECK_MODULES(CURL, libcurl) +AC_SUBST(CURL_CFLAGS) +AC_SUBST(CURL_LIBS) + +# We need OpenSSL for crypto and HTTPS callouts. +PKG_CHECK_MODULES(OPENSSL, openssl) +AC_SUBST(OPENSSL_CFLAGS) +AC_SUBST(OPENSSL_LIBS) + +# Create Makefile from Makefile.in +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/debian/auth_openidc.conf b/debian/auth_openidc.conf new file mode 100644 index 00000000..e90db9de --- /dev/null +++ b/debian/auth_openidc.conf @@ -0,0 +1,277 @@ +######################################################################################## +# +# Common Settings +# +######################################################################################## + +# (Mandatory) +# The redirect_uri for this OpenID Connect client; must point to a path on your server +# protected by this module but does not front/return any actual content. +#OIDCRedirectURI https://www.example.com/protected/redirect_uri + +# (Mandatory) +# Set a password for crypto purposes, used in state and (optionally) by-value session cookies. +#OIDCCryptoPassphrase + +# (Optional) +# When using multiple OpenID Connect Providers, possibly combined with Dynamic Client +# Registration and account-based OP Discovery. +# Directory that holds metadata files (must be writable for the Apache process/user) +# When not specified, it is assumed that we use a single statically configured provider +# as described under the section "OpenID Connect Provider" below. +OIDCMetadataDir /var/cache/apache2/mod_auth_openidc/metadata + + +######################################################################################## +# +# (Optional) +# +# OpenID Connect Client +# +# Settings used by the client in communication with the OpenID Connect Provider(s), +# cq. in Authorization Requests, Dynamic Client Registration and UserInfo Endpoint access. +# +######################################################################################## + +# (Optional) +# Require a valid SSL server certificate when communicating with the OP. +# (cq. on token endpoint, UserInfo endpoint and Dynamic Client Registration endpoint) +# When not defined, the default value is "On". +# NB: this can be overridden on a per-client basis in the client metadata using the proprietary key: ssl_validate_server +#OIDCSSLValidateServer [On|Off] + +# (Optional) +# The response type (or OpenID Connect Flow) used: must be one of \"code\", \"id_token\", or \"id_token token\". +# (this serves as default value for discovered OPs too) +# When not defined the "code" response type is used. +#OIDCResponseType [code|id_token|id_token token] + +# (Optional) +# Only used for a single static provider has been configured, see below in OpenID Connect Provider. +# Client identifier used in calls to the statically configured OpenID Connect Provider. +#OIDCClientID + +# (Optional) +# Only used for a single static provider has been configured, see below in OpenID Connect Provider. +# Client secret used in calls to the statically configured OpenID Connect Provider. +# (not used/required in the Implicit Client Profile, cq. when OIDCResponseType is "code") +#OIDCClientSecret + +# (Optional) +# The client name that the client registers in dynamic registration with the OP. +# When not defined, no client name will be sent with the registration request. +#OIDCClientName + +# (Optional) +# The contacts that the client registers in dynamic registration with the OP. +# Must be formatted as e-mail addresses by specification. +# Single value only; when not defined, no contact e-mail address will be sent with the registration request. +#OIDCClientContact + +# (Optional) +# Define the OpenID Connect scope that is requested from the OP (eg. "openid email profile"). +# When not defined, the bare minimal scope "openid" is used. +# NB: this can be overridden on a per-client basis in the client metadata using the proprietary key: scope +#OIDCScope + +######################################################################################## +# +# (Optional) +# +# OpenID Connect Provider +# +# When configuration a single static provider and not using OpenID Connect Provider Discovery. +# +######################################################################################## + +# OpenID Connect Provider issuer identifier (eg. https://localhost:9031 or accounts.google.com) +#OIDCProviderIssuer + +# OpenID Connect Provider Authorization Endpoint URL (eg. https://localhost:9031/as/authorization.oauth2) +#OIDCProviderAuthorizationEndpoint + +# OpenID Connect Provider JWKS URL (eg. https://localhost:9031/pf/JWKS) +# cq. the URL on which the signing keys for this OP are hosted, in JWK formatting +#OIDCProviderJwksUri + +# (Optional) +# OpenID Connect Provider Token Endpoint URL (eg. https://localhost:9031/as/token.oauth2) +#OIDCProviderTokenEndpoint + +# (Optional) +# Authentication method for the OpenID OP Token Endpoint (eg. "client_secret_basic" or "client_secret_post") +# When not defined the default method from the specification is used, cq. "client_secret_basic". +#OIDCProviderTokenEndpointAuth + +# (Optional) +# OpenID Connect Provider UserInfo Endpoint URL (eg. https://localhost:9031/idp/userinfo.openid) +# When not defined no claims will be resolved from such endpoint. +#OIDCProviderUserInfoEndpoint + +######################################################################################## +# +# (Optional) +# +# OAuth 2.0 Settings +# +# Used when this module functions as a Resource Server against a PingFederate Authorization +# Server, validating Bearer reference tokens. +# +######################################################################################## + +# Client identifier used in token validation calls to the OAuth 2.0 Authorization server. +#OIDCOAuthClientID + +# Client secret used in token validation calls to the OAuth 2.0 Authorization server. +#OIDCOAuthClientSecret + +# OAuth 2.0 Authorization Server token validation endpoint (eg. https://localhost:9031/as/token.oauth2) +#OIDCOAuthEndpoint + +# (Optional) +# Authentication method for the OAuth 2.0 Authorization Server validation endpoint, +# cq. either "client_secret_basic" or "client_secret_post; when not defined "client_secret_basic" is used. +#OIDCOAuthEndpointAuth + +# (Optional) +# Require a valid SSL server certificate when communicating with the Authorization Server. +# (cq. on the token validation endpoint) +# When not defined, the default value is "On". +#OIDCOAuthSSLValidateServer [On|Off] + + +######################################################################################## +# +# (Optional) +# +# Cache Settings +# +######################################################################################## + +# (Optional) +# Cache type, used for temporary storage that is shared across Apache processes/servers for: +# a) session state +# b) issued nonce values to prevent replay attacks +# c) validated OAuth 2.0 tokens +# d) JWK sets that have been retrieved from jwk_uri's +# must be one of \"file\", \"memcache\" or \"shm\". When not defined, "file" is used. +#OIDCCacheType [file|memcache|shm] + +# (Optional) +# When using OIDCCacheType "file": +# Directory that holds cache files for session state and validated OAuth 2.0 tokens +# (must be writable for the Apache process/user) +# When not specified a system defined temporary directory (/tmp) will be used. +#OIDCCacheDir /var/cache/apache2/mod_auth_openidc/cache + +# (Optional) +# When using OIDCCacheType "file": +# Cache file clean interval in seconds (only triggered on writes). +# When not specified a default of 60 seconds is used. +# OIDCCacheFileCleanInterval + +# (Optional) +# Required when using OIDCCacheType "memcache": +# Specifies the memcache servers used for caching (space separated list of [:] tuples) +#OIDCMemCacheServers ([:])+ + +# (Optional) +# When using OIDCCacheType "shm": +# Specifies the maximum number of name/value pair entries that can be cached. +# When not specified, a default of 500 entries is used. +# OIDCCacheShmMax + +######################################################################################## +# +# (Optional) +# +# Advanced Settings +# +######################################################################################## + +# (Optional) +# OpenID Connect session storage type. +# "file" uses file based server-side caching storage. +# "cookie" uses browser-side sessions stored in a cookie. +# When not defined the default "file" is used. +#OIDCSessionType [file|cookie] + +# (Optional) +# Defines an external OP Discovery page. That page will be called with: +# ?oidc_return=&oidc_callback= +# +# An Issuer selection can be passed back to the callback URL as in: +# ?oidc_return=&oidc_provider=[${issuer}|${domain}|${e-mail-style-account-name}] +# where the parameter contains the URL-encoded issuer value of +# the selected Provider, or a URL-encoded account name for OpenID +# Connect Discovery purposes (aka. e-mail style identifier), or a domain name. +# +# When not defined the bare-bones internal OP Discovery page is used. +#OIDCDiscoverURL + +# (Optional) +# The algorithm that the OP should use to sign the id_token (used only in dynamic client registration) +# When not defined the default is RS256. +#OIDCIDTokenAlg [RS256|RS384|RS512|PS256|PS384|PS512] + +# (Optional) +# the refresh interval in seconds for the JWKs key set that obtained from jwk_uris +# When not defined the default is 3600 seconds. +# NB: this can be overridden on a per-client basis in the client metadata using the proprietary key: jwks_refresh_interval +#OIDCJWKSRefreshInterval + +# (Optional) +# Acceptable offset (both before and after) for checking the \"iat\" (= issued at) timestamp in the id_token. +# When not defined the default is 600 seconds. +# NB: this can be overridden on a per-client basis in the client metadata using the proprietary key: idtoken_iat_slack +#OIDCIDTokenIatSlack + +# (Optional) +# Define the cookie name for the session cookie. +# When not defined the default is "mod-auth-connect". +#OIDCCookie + +# (Optional) +# Specify domain element for OIDC session cookie. +# When not defined the default is the server name. +#OIDCCookieDomain + +# (Optional) +# Define the cookie path for the session cookie. +# When not defined the default is the current path. +#OIDCCookiePath + +# (Optional) +# The prefix to use when setting claims in the HTTP headers. +# When not defined, the default "OIDC_CLAIM_" is used. +#OIDCClaimPrefix + +# (Optional) +# The delimiter to use when setting multi-valued claims in the HTTP headers. +# When not defined the default "," is used. +#OIDCClaimDelimiter + +# (Optional) +# Specify the HTTP header variable name to set with the name of the authenticated user, +# cq. the "sub" claim in the id_token. When not defined no such header is added. +#OIDCAuthNHeader + +# (Optional) +# Timeout in seconds for long duration HTTP calls. This is used for most outgoing calls. +# When not defined the default of 60 seconds is used. +#OIDCHTTPTimeoutLong + +# (Optional) +# Timeout in seconds for short duration HTTP calls; used for Client Registration and OP Discovery calls. +# When not defined the default of 5 seconds is used. +#OIDCHTTPTimeoutShort + +# (Optional) +# Time to live in seconds for state parameter cq. the interval in which the authorization request +# and the corresponding response need to be completed. When not defined the default of 300 seconds is used. +#OIDCStateTimeout + +# (Optional) +# Scrub user name and claim headers (as configured above) from the user's request. +# The default is "On"; use "Off" only for testing and debugging because it renders your system insecure. +#OIDCScrubRequestHeaders [On|Off] \ No newline at end of file diff --git a/debian/auth_openidc.load b/debian/auth_openidc.load new file mode 100644 index 00000000..64ea879a --- /dev/null +++ b/debian/auth_openidc.load @@ -0,0 +1 @@ +LoadModule auth_openidc_module /usr/lib/apache2/modules/mod_auth_openidc.so diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..1c4bb20b --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +libapache2-auth-openidc (1.0) unstable; urgency=low + + * Initial release under new name and flag. + + -- Hans Zandbelt Wed, 27 Mar 2014 21:00:00 +0100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 00000000..45a4fb75 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..1fd07dd3 --- /dev/null +++ b/debian/control @@ -0,0 +1,16 @@ +Source: libapache2-mod-auth-openidc +Priority: extra +Maintainer: Hans Zandbelt +Build-Depends: debhelper (>= 8.0.0), autotools-dev, dh-apache2, apache2-dev, libcurl3-dev +Standards-Version: 3.9.4 +Section: web +Homepage: https://github.com/pingidentity/mod_auth_openidc +#Vcs-Git: https://github.com/pingidentity/mod_auth_openidc.git + +Package: libapache2-mod-auth-openidc +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: OpenID Connect authentication module for Apache + mod_auth_openidc is an Apache module that enables you to authenticate + users of a web site against an OpenID Connect Identity Provider. It can grant + access to paths and provide attributes to other modules and applications. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..f3f6513d --- /dev/null +++ b/debian/copyright @@ -0,0 +1,28 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: mod-auth-openidc +Upstream-Contact: Hans Zandbelt +Source: https://github.com/pingidentity/mod_auth_openidc + +Files: * +Copyright: 2013-2014 Hans Zandbelt +License: Apache-2.0 + +Files: debian/* +Copyright: 2014 Hans Zandbelt +License: Apache-2.0 + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache License 2.0 can + be found in "/usr/share/common-licenses/Apache-2.0" diff --git a/debian/dirs b/debian/dirs new file mode 100644 index 00000000..617efef8 --- /dev/null +++ b/debian/dirs @@ -0,0 +1,3 @@ +usr/lib/apache2/modules +var/cache/apache2/mod_auth_openidc +var/cache/apache2/mod_auth_openidc/metadata diff --git a/debian/docs b/debian/docs new file mode 100644 index 00000000..fc82724a --- /dev/null +++ b/debian/docs @@ -0,0 +1,2 @@ +README +ChangeLog diff --git a/debian/libapache2-mod-auth-openidc.apache2 b/debian/libapache2-mod-auth-openidc.apache2 new file mode 100644 index 00000000..fc5f37f3 --- /dev/null +++ b/debian/libapache2-mod-auth-openidc.apache2 @@ -0,0 +1,3 @@ +mod src/.libs/mod_auth_openidc.so +mod debian/auth_openidc.load +mod debian/auth_openidc.conf diff --git a/debian/lintian-overrides b/debian/lintian-overrides new file mode 100644 index 00000000..4f586c66 --- /dev/null +++ b/debian/lintian-overrides @@ -0,0 +1,2 @@ +libapache2-mod-auth-openidc: non-standard-dir-perm var/cache/apache2/mod_auth_openidc/ 0700 != 0755 +libapache2-mod-auth-openidc: non-standard-dir-perm var/cache/apache2/mod_auth_openidc/metadata/ 0700 != 0755 diff --git a/debian/rules b/debian/rules new file mode 100755 index 00000000..58ccc1e9 --- /dev/null +++ b/debian/rules @@ -0,0 +1,23 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +%: + dh $@ --with apache2 + +override_dh_apache2: + dh_apache2 --noscripts + +override_dh_auto_install: + +override_dh_fixperms: + dh_fixperms + chown -R www-data debian/libapache2-mod-auth-openidc/var/cache/apache2/mod_auth_openidc + chmod -R go= debian/libapache2-mod-auth-openidc/var/cache/apache2/mod_auth_openidc diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 00000000..89ae9db8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 00000000..4f7a508b --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,5 @@ +/*.lo +/*.o +/*.slo +/*.la +/.libs diff --git a/src/apr_json.h b/src/apr_json.h new file mode 100644 index 00000000..1361ae0d --- /dev/null +++ b/src/apr_json.h @@ -0,0 +1,156 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef APR_JSON_H +#define APR_JSON_H + +/** + * @file apr_json.h + * @brief APR-JSON routines + */ + +#include "apr.h" +#include "apr_pools.h" +#include "apr_tables.h" +#include "apr_hash.h" +#include "apr_strings.h" +#include "apr_buckets.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @defgroup APR_JSON JSON functions + * @{ + */ + +/** + * APJ_DECLARE_EXPORT is defined when building the APR-JSON dynamic library, + * so that all public symbols are exported. + * + * APJ_DECLARE_STATIC is defined when including the APR-JSON public headers, + * to provide static linkage when the dynamic library may be unavailable. + * + * APJ_DECLARE_STATIC and APJ_DECLARE_EXPORT are left undefined when + * including the APR-JSON public headers, to import and link the symbols from + * the dynamic APR-JSON library and assure appropriate indirection and calling + * conventions at compile time. + */ + +#if defined(DOXYGEN) || !defined(WIN32) +/** + * The public APR-JSON functions are declared with APJ_DECLARE(), so they may + * use the most appropriate calling convention. Public APR functions with + * variable arguments must use APJ_DECLARE_NONSTD(). + */ +#define APJ_DECLARE(type) type +/** + * The public APR-JSON functions using variable arguments are declared with + * APJ_DECLARE_NONSTD(), as they must use the C language calling convention. + */ +#define APJ_DECLARE_NONSTD(type) type +/** + * The public APR-JSON variables are declared with APJ_DECLARE_DATA. + * This assures the appropriate indirection is invoked at compile time. + * + * @note APJ_DECLARE_DATA extern type apr_variable; syntax is required for + * declarations within headers to properly import the variable. + */ +#define APJ_DECLARE_DATA +#else + +#if defined(APJ_MODULE_DECLARE_STATIC) +#define APJ_DECLARE(type) type __stdcall +#define APJ_DECLARE_NONSTD(type) type __cdecl +#define APJ_DECLARE_DATA +#elif defined(APJ_DECLARE_EXPORT) +#define APJ_DECLARE(type) __declspec(dllexport) type __stdcall +#define APJ_DECLARE_NONSTD(type) __declspec(dllexport) type __cdecl +#define APJ_DECLARE_DATA __declspec(dllexport) +#else +#define APJ_DECLARE(type) __declspec(dllimport) type __stdcall +#define APJ_DECLARE_NONSTD(type) __declspec(dllimport) type __cdecl +#define APJ_DECLARE_DATA __declspec(dllimport) +#endif + +#endif /* defined(DOXYGEN) || !defined(WIN32) */ + +/** + * Enum that represents the type of the given JSON value. + */ +typedef enum apr_json_type_e { + APR_JSON_OBJECT, + APR_JSON_ARRAY, + APR_JSON_STRING, + APR_JSON_LONG, + APR_JSON_DOUBLE, + APR_JSON_BOOLEAN, + APR_JSON_NULL +} apr_json_type_e; + +/** + * A structure to hold a JSON string. + */ +typedef struct apr_json_string_t { + /** pointer to the buffer */ + const char *p; + /** string length */ + apr_size_t len; +} apr_json_string_t; + +/** + * A structure that holds a JSON value. + */ +typedef struct apr_json_value_t { + /** type of the value */ + apr_json_type_e type; + /** actual value. which member is valid depends on type. */ + union { + apr_hash_t *object; + apr_array_header_t *array; + double dnumber; + long lnumber; + apr_json_string_t string; + int boolean; + } value; +} apr_json_value_t; + +/** + * Decode utf8-encoded JSON string into apr_json_value_t + * @param retval the result + * @param injson utf8-encoded JSON string. + * @param size length of the input string. + * @param pool pool used to allocate the result from. + */ +APJ_DECLARE(apr_status_t) apr_json_decode(apr_json_value_t **retval, const char *injson, apr_size_t size, apr_pool_t *pool); + +/** + * Encode data represented as apr_json_value_t to utf8-encoded JSON string + * and append it to the specified brigade + * @param brigade brigade the result will be appended to. + * @param json the JSON data. + * @param pool pool used to allocate the buckets from. + */ +APJ_DECLARE(void) apr_json_encode(apr_bucket_brigade *brigade, const apr_json_value_t *json, apr_pool_t *pool); + +/** @} */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* !APR_JSON_H */ diff --git a/src/apr_json_decode.c b/src/apr_json_decode.c new file mode 100644 index 00000000..b4cb308e --- /dev/null +++ b/src/apr_json_decode.c @@ -0,0 +1,695 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "apr_json.h" + +typedef struct _json_link_t { + apr_json_value_t *value; + struct _json_link_t *next; +} json_link_t; + +typedef struct apr_json_scanner_t { + const char *p; + const char *e; + apr_pool_t *pool; +} apr_json_scanner_t; + +static apr_status_t apr_json_decode_value(apr_json_scanner_t *self, apr_json_value_t **retval); + +/* stolen from mod_mime_magic.c :) */ +/* Single hex char to int; -1 if not a hex char. */ +static int hex_to_int(int c) +{ + if (isdigit(c)) + return c - '0'; + if ((c >= 'a') && (c <= 'f')) + return c + 10 - 'a'; + if ((c >= 'A') && (c <= 'F')) + return c + 10 - 'A'; + return -1; +} + +static apr_ssize_t ucs4_to_utf8(char *out, int code) +{ + if (code < 0x00000080) { + out[0] = code; + return 1; + } else if (code < 0x00000800) { + out[0] = 0xc0 + (code >> 6); + out[1] = 0x80 + (code & 0x3f); + return 2; + } else if (code < 0x00010000) { + out[0] = 0xe0 + (code >> 12); + out[1] = 0x80 + ((code >> 6) & 0x3f) ; + out[2] = 0x80 + (code & 0x3f); + return 3; + } else if (code < 0x00200000) { + out[0] = 0xd0 + (code >> 18); + out[1] = 0x80 + ((code >> 12) & 0x3f); + out[2] = 0x80 + ((code >> 6) & 0x3f); + out[3] = 0x80 + (code & 0x3F); + return 4; + } + return -1; +} + +static apr_status_t apr_json_decode_string(apr_json_scanner_t *self, apr_json_string_t *retval) +{ + apr_status_t status = APR_SUCCESS; + apr_json_string_t string; + const char *p, *e; + char *q; + + if (self->p >= self->e) { + status = APR_EOF; + goto out; + } + + self->p++; //advance past the \" + string.len = 0; + for (p = self->p, e = self->e; p < e;) { + if (*p == '"') + break; + else if (*p == '\\') { + p++; + if (p >= e) { + status = APR_EOF; + goto out; + } + if (*p == 'u') { + if (p + 4 >= e) { + status = APR_EOF; + goto out; + } + p += 5; + string.len += 4; /* an UTF-8 character spans at most 4 bytes */ + break; + } else { + string.len++; + p++; + } + } + else { + string.len++; + p++; + } + } + + string.p = q = apr_pcalloc(self->pool, string.len + 1); + e = p; + +#define VALIDATE_UTF8_SUCCEEDING_BYTE(p) \ + if (*(unsigned char *)(p) < 0x80 || *(unsigned char *)(p) >= 0xc0) { \ + status = APR_EGENERAL; \ + goto out; \ + } + + for (p = self->p; p < e;) { + switch (*(unsigned char *)p) { + case '\\': + p++; + switch(*p) { + case 'u': + /* THIS IS REQUIRED TO BE A 4 DIGIT HEX NUMBER */ + { + int cp = 0; + while (p < e) { + int d = hex_to_int(*p); + if (d < 0) { + status = APR_EGENERAL; + goto out; + } + cp = (cp << 4) | d; + p++; + } + if (cp >= 0xd800 && cp < 0xdc00) { + /* surrogate pair */ + int sc = 0; + if (p + 6 > e) { + status = APR_EOF; + goto out; + } + if (p[0] != '\\' && p[1] != 'u') { + status = APR_EGENERAL; + goto out; + } + while (p < e) { + int d = hex_to_int(*p); + if (d < 0) { + status = APR_EGENERAL; + goto out; + } + sc = (sc << 4) | d; + p++; + } + cp = ((cp & 0x3ff) << 10) | (sc & 0x3ff); + if ((cp >= 0xd800 && cp < 0xe000) || (cp >= 0x110000)) { + status = APR_EGENERAL; + goto out; + } + } else if (cp >= 0xdc00 && cp < 0xe000) { + status = APR_EGENERAL; + goto out; + } + q += ucs4_to_utf8(q, cp); + } + break; + case '\\': + *q++ = '\\'; + p++; + break; + case '/': + *q++ = '/'; + p++; + break; + case 'n': + *q++ = '\n'; + p++; + break; + case 'r': + *q++ = '\r'; + p++; + break; + case 't': + *q++ = '\t'; + p++; + break; + case 'f': + *q++ = '\f'; + p++; + break; + case 'b': + *q++ = '\b'; + p++; + break; + case '"': + *q++ = '"'; + p++; + break; + default: + status = APR_EGENERAL; + goto out; + } + break; + + case 0xc0: case 0xc1: case 0xc2: case 0xc3: + case 0xc4: case 0xc5: case 0xc6: case 0xc7: + case 0xc8: case 0xc9: case 0xca: case 0xcb: + case 0xcc: case 0xcd: case 0xce: case 0xcf: + case 0xd0: case 0xd1: case 0xd2: case 0xd3: + case 0xd4: case 0xd5: case 0xd6: case 0xd7: + case 0xd8: case 0xd9: case 0xda: case 0xdb: + case 0xdc: case 0xdd: case 0xde: case 0xdf: + if (p + 1 >= e) { + status = APR_EOF; + goto out; + } + *q++ = *p++; + VALIDATE_UTF8_SUCCEEDING_BYTE(p); + *q++ = *p++; + break; + + case 0xe0: case 0xe1: case 0xe2: case 0xe3: + case 0xe4: case 0xe5: case 0xe6: case 0xe7: + case 0xe8: case 0xe9: case 0xea: case 0xeb: + case 0xec: case 0xed: case 0xee: case 0xef: + if (p + 2 >= e) { + status = APR_EOF; + goto out; + } + *q++ = *p++; + VALIDATE_UTF8_SUCCEEDING_BYTE(p); + *q++ = *p++; + VALIDATE_UTF8_SUCCEEDING_BYTE(p); + *q++ = *p++; + break; + + case 0xf0: case 0xf1: case 0xf2: case 0xf3: + case 0xf4: case 0xf5: case 0xf6: case 0xf7: + if (p + 3 >= e) { + status = APR_EOF; + goto out; + } + if (((unsigned char *)p)[0] >= 0xf5 || ((unsigned char *)p)[1] >= 0x90) { + status = APR_EGENERAL; + goto out; + } + *q++ = *p++; + VALIDATE_UTF8_SUCCEEDING_BYTE(p); + *q++ = *p++; + VALIDATE_UTF8_SUCCEEDING_BYTE(p); + *q++ = *p++; + VALIDATE_UTF8_SUCCEEDING_BYTE(p); + *q++ = *p++; + break; + + case 0xf8: case 0xf9: case 0xfa: case 0xfb: + if (p + 4 >= e) { + status = APR_EOF; + goto out; + } + status = APR_EGENERAL; + goto out; + + case 0xfc: case 0xfd: + if (p + 5 >= e) { + status = APR_EOF; + goto out; + } + status = APR_EGENERAL; + goto out; + + default: + *q++ = *p++; + break; + } + } +#undef VALIDATE_UTF8_SUCCEEDING_BYTE + *p++; /* eat the trailing '"' */ + *retval = string; +out: + self->p = p; + return status; +} + +static apr_status_t apr_json_decode_array(apr_json_scanner_t *self, apr_array_header_t **retval) +{ + apr_status_t status = APR_SUCCESS; + apr_pool_t *link_pool = NULL; + json_link_t *head = NULL, *tail = NULL; + apr_size_t count = 0; + + if ((status = apr_pool_create(&link_pool, self->pool))) + return status; + + if (self->p >= self->e) { + status = APR_EOF; + goto out; + } + + self->p++; /* toss of the leading [ */ + + for (;;) { + apr_json_value_t *element; + json_link_t *new_node; + + while (self->p < self->e && isspace(*(unsigned char *)self->p)) + self->p++; + + if (self->p == self->e) { + status = APR_EOF; + goto out; + } + + if (*self->p == ']') { + self->p++; + break; + } + + if ((status = apr_json_decode_value(self, &element))) { + status = APR_EGENERAL; + goto out; + } + + new_node = apr_pcalloc(link_pool, sizeof(json_link_t)); + new_node->value = element; + if (tail) { + tail->next = new_node; + } else { + head = new_node; + } + tail = new_node; + count++; + + while (self->p < self->e && isspace(*(unsigned char *)self->p)) + self->p++; + + if (self->p == self->e) { + status = APR_EOF; + goto out; + } + + if (*self->p == ',') { + self->p++; + } else if (*self->p != ']') { + status = APR_EGENERAL; + goto out; + } + } + + { + json_link_t *node; + apr_array_header_t *array = apr_array_make(self->pool, count, sizeof(apr_json_value_t *)); + for (node = head; node; node = node->next) + *((apr_json_value_t**)(apr_array_push(array))) = node->value; + *retval = array; + } +out: + if (link_pool) + apr_pool_destroy(link_pool); + return status; +} + +static apr_status_t apr_json_decode_object(apr_json_scanner_t *self, apr_hash_t **retval) +{ + apr_status_t status = APR_SUCCESS; + apr_hash_t *object = apr_hash_make(self->pool); + + if (self->p >= self->e) + return APR_EOF; + + self->p++; /* toss of the leading [ */ + + for (;;) { + apr_json_string_t name; + apr_json_value_t *value; + + while (self->p < self->e && isspace(*(unsigned char *)self->p)) + self->p++; + + if (self->p == self->e) { + status = APR_EOF; + goto out; + } + + if (*self->p == '}') { + self->p++; + break; + } + + if (*self->p != '"') { + status = APR_EGENERAL; + goto out; + } + + if ((status = apr_json_decode_string(self, &name))) + goto out; + + while (self->p < self->e && isspace(*(unsigned char *)self->p)) + self->p++; + + if (self->p == self->e) { + status = APR_EOF; + goto out; + } + + if (*self->p != ':') { + status = APR_EGENERAL; + goto out; + } + + self->p++; /* eat the ':' */ + + while (self->p < self->e && isspace(*(unsigned char *)self->p)) + self->p++; + + if (self->p == self->e) { + status = APR_EOF; + goto out; + } + + if ((status = apr_json_decode_value(self, &value))) + goto out; + + apr_hash_set(object, name.p, name.len, value); + + while (self->p < self->e && isspace(*(unsigned char *)self->p)) + self->p++; + + if (self->p == self->e) { + status = APR_EOF; + goto out; + } + + if (*self->p == ',') { + self->p++; + } else if (*self->p != '}') { + status = APR_EGENERAL; + goto out; + } + } + + *retval = object; +out: + return status; +} + +apr_status_t apr_json_decode_boolean(apr_json_scanner_t *self, int *retval) +{ + if (self->p >= self->e) + return APR_EOF; + + if (self->e - self->p >= 4 && strncmp("true", self->p, 4) == 0 && + (self->p == self->e || (!isalnum(((unsigned char *)self->p)[4]) && ((unsigned char *)self->p)[4] != '_'))) { + self->p += 4; + *retval = 1; + return APR_SUCCESS; + } else if (self->e - self->p >= 5 && strncmp("false", self->p, 5) == 0 && + (self->p == self->e || (!isalnum(((unsigned char *)self->p)[5]) && ((unsigned char *)self->p)[5] != '_'))) { + self->p += 5; + *retval = 0; + return APR_SUCCESS; + } + return APR_EGENERAL; +} + +apr_status_t apr_json_decode_number(apr_json_scanner_t *self, apr_json_value_t *retval) +{ + apr_status_t status = APR_SUCCESS; + int treat_as_float = 0, exp_occurred = 0; + const char *p = self->p, *e = self->e; + + if (p >= e) + return APR_EOF; + + { + unsigned char c = *(unsigned char *)p; + if (c == '-') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + } + if (c == '.') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + treat_as_float = 1; + } + if (!isdigit(c)) { + status = APR_EGENERAL; + goto out; + } + p++; + } + + if (!treat_as_float) { + while (p < e) { + unsigned char c = *(unsigned char *)p; + if (c == 'e' || c == 'E') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + if (c == '-') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + } + if (!isdigit(c)) { + status = APR_EGENERAL; + goto out; + } + treat_as_float = 1; + exp_occurred = 1; + break; + } + else if (c == '.') { + p++; + treat_as_float = 1; + break; + } + else if (!isdigit(c)) + break; + p++; + } + } else { + while (p < e) { + unsigned char c = *(unsigned char *)p; + if (c == 'e' || c == 'E') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + if (c == '-') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + } + if (!isdigit(c)) { + status = APR_EGENERAL; + goto out; + } + exp_occurred = 1; + break; + } + else if (!isdigit(c)) + break; + p++; + } + } + + if (treat_as_float) { + if (!exp_occurred) { + while (p < e) { + unsigned char c = *(unsigned char *)p; + if (c == 'e' || c == 'E') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + if (c == '-') { + p++; + if (p >= e) + return APR_EOF; + c = *(unsigned char *)p; + } + if (!isdigit(c)) { + status = APR_EGENERAL; + goto out; + } + exp_occurred = 1; + break; + } + else if (!isdigit(c)) + break; + p++; + } + } + if (exp_occurred) { + if (p >= e || !isdigit(*(unsigned char *)p)) + return APR_EOF; + while (++p < e && isdigit(*(unsigned char *)p)); + } + } + + if (treat_as_float) { + retval->type = APR_JSON_DOUBLE; + retval->value.dnumber = strtod(self->p, NULL); + } else { + retval->type = APR_JSON_LONG; + retval->value.lnumber = strtol(self->p, NULL, 10); + } + +out: + self->p = p; + return status; +} + +apr_status_t apr_json_decode_null(apr_json_scanner_t *self) +{ + if (self->e - self->p >= 4 && strncmp("null", self->p, 4) == 0 && + (self->p == self->e || (!isalnum(((unsigned char *)self->p)[4]) && ((unsigned char *)self->p)[4] != '_'))) { + self->p += 4; + return APR_SUCCESS; + } + return APR_EGENERAL; +} + +static apr_status_t apr_json_decode_value(apr_json_scanner_t *self, apr_json_value_t **retval) +{ + apr_json_value_t value; + apr_status_t status; + + while (self->p < self->e && isspace(*(unsigned char *)self->p)) + self->p++; + + switch (*(unsigned char *)self->p) { + case '"': + value.type = APR_JSON_STRING; + status = apr_json_decode_string(self, &value.value.string); + break; + case '[': + value.type = APR_JSON_ARRAY; + status = apr_json_decode_array(self, &value.value.array); + break; + case '{': + value.type = APR_JSON_OBJECT; + status = apr_json_decode_object(self, &value.value.object); + break; + case 'n': + value.type = APR_JSON_NULL; + break; + case 't': + case 'f': + value.type = APR_JSON_BOOLEAN; + status = apr_json_decode_boolean(self, &value.value.boolean); + break; + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + status = apr_json_decode_number(self, &value); + break; + default: + status = APR_EGENERAL; + break; + } + + if (status == APR_SUCCESS) { + *retval = apr_palloc(self->pool, sizeof(apr_json_value_t)); + **retval = value; + } + return status; +} + +apr_status_t apr_json_decode(apr_json_value_t **retval, const char *injson, apr_size_t injson_size, apr_pool_t *pool) +{ + apr_status_t status; + apr_pool_t *subpool; + apr_json_scanner_t scanner; + + if ((status = apr_pool_create(&subpool, pool))) + return status; + + scanner.p = injson; + scanner.e = injson + injson_size; + scanner.pool = subpool; + if ((status = apr_json_decode_value(&scanner, retval))) { + apr_pool_destroy(subpool); + return status; + } + while (scanner.p < scanner.e && isspace(*(unsigned char *)scanner.p)) + scanner.p++; + if (scanner.p != scanner.e) { + /* trailing craft */ + apr_pool_destroy(subpool); + return APR_EGENERAL; + } + return APR_SUCCESS; +} diff --git a/src/authz.c b/src/authz.c new file mode 100644 index 00000000..83a1ad0a --- /dev/null +++ b/src/authz.c @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * mostly copied from mod_auth_cas + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include +#include + +#include "mod_auth_openidc.h" + +/* the name of the keyword that follows the Require primitive to indicate claims-based authorization */ +#define OIDC_REQUIRE_NAME "claim" + +/* + * see if a the Require value matches with a set of provided claims + */ +static apr_byte_t oidc_authz_match_claim(request_rec *r, + const char * const attr_spec, const apr_json_value_t * const claims) { + + apr_hash_index_t *hi; + const void *key; + apr_ssize_t klen; + void *hval; + + /* if we don't have any claims, they can never match any Require claim primitive */ + if (claims == NULL) + return FALSE; + + /* loop over all of the user claims */ + for (hi = apr_hash_first(r->pool, claims->value.object); hi; hi = + apr_hash_next(hi)) { + apr_hash_this(hi, &key, &klen, &hval); + + const char *attr_c = (const char *) key; + const char *spec_c = attr_spec; + + /* walk both strings until we get to the end of either or we find a differing character */ + while ((*attr_c) && (*spec_c) && (*attr_c) == (*spec_c)) { + attr_c++; + spec_c++; + } + + /* The match is a success if we walked the whole claim name and the attr_spec is at a colon. */ + if (!(*attr_c) && (*spec_c) == ':') { + const apr_json_value_t *val; + + val = ((apr_json_value_t *) hval); + + /* skip the colon */ + spec_c++; + + /* see if it is a string and it (case-insensitively) matches the Require'd value */ + if (val->type == APR_JSON_STRING) { + + if (apr_strnatcmp(val->value.string.p, spec_c) == 0) { + return TRUE; + } + + /* see if it is a boolean and it (case-insensitively) matches the Require'd value */ + } else if (val->type == APR_JSON_BOOLEAN) { + + if (apr_strnatcmp(val->value.boolean ? "true" : "false", spec_c) + == 0) { + return TRUE; + } + + /* if it is an array, we'll walk it */ + } else if (val->type == APR_JSON_ARRAY) { + + /* compare the claim values */ + int i = 0; + for (i = 0; i < val->value.array->nelts; i++) { + + apr_json_value_t *elem = + APR_ARRAY_IDX(val->value.array, i, apr_json_value_t *); + + if (elem->type == APR_JSON_STRING) { + /* + * approximately compare the claim value (ignoring + * whitespace). At this point, spec_c points to the + * NULL-terminated value pattern. + */ + if (apr_strnatcmp(elem->value.string.p, spec_c) == 0) { + return TRUE; + } + + } else if (elem->type == APR_JSON_BOOLEAN) { + + if (apr_strnatcmp( + elem->value.boolean ? "true" : "false", spec_c) + == 0) { + return TRUE; + } + + } else { + + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_authz_match_claim: unhandled in-array JSON object type [%d] for key \"%s\"", + elem->type, (const char *) key); + continue; + } + } + + } else { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_authz_match_claim: unhandled JSON object type [%d] for key \"%s\"", + val->type, (const char *) key); + continue; + } + + } + /* TODO: a tilde (denotes a PCRE match). */ + //else if (!(*attr_c) && (*spec_c) == '~') { + } + return FALSE; +} + +/* + * Apache <2.4 authorizatio routine: match the claims from the authenticated user against the Require primitive + */ +int oidc_authz_worker(request_rec *r, const apr_json_value_t * const claims, + const require_line * const reqs, int nelts) { + const int m = r->method_number; + const char *token; + const char *requirement; + int i; + int have_oauthattr = 0; + int count_oauth_claims = 0; + + /* go through applicable Require directives */ + for (i = 0; i < nelts; ++i) { + + /* ignore this Require if it's in a section that exclude this method */ + if (!(reqs[i].method_mask & (AP_METHOD_BIT << m))) { + continue; + } + + /* ignore if it's not a "Require claim ..." */ + requirement = reqs[i].requirement; + + token = ap_getword_white(r->pool, &requirement); + + if (apr_strnatcasecmp(token, OIDC_REQUIRE_NAME) != 0) { + continue; + } + + /* ok, we have a "Require claim" to satisfy */ + have_oauthattr = 1; + + /* + * If we have an applicable claim, but no claims were sent in the request, then we can + * just stop looking here, because it's not satisfiable. The code after this loop will + * give the appropriate response. + */ + if (!claims) { + break; + } + + /* + * iterate over the claim specification strings in this require directive searching + * for a specification that matches one of the claims. + */ + while (*requirement) { + token = ap_getword_conf(r->pool, &requirement); + count_oauth_claims++; + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authz_worker: evaluating claim specification: %s", + token); + + if (oidc_authz_match_claim(r, token, claims) == TRUE) { + + /* if *any* claim matches, then authorization has succeeded and all of the others are ignored */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authz_worker: require claim " + "'%s' matched", token); + return OK; + } + } + } + + /* if there weren't any "Require claim" directives, we're irrelevant */ + if (!have_oauthattr) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authz_worker: no claim statements found, not performing authz"); + return DECLINED; + } + /* if there was a "Require claim", but no actual claims, that's cause to warn the admin of an iffy configuration */ + if (count_oauth_claims == 0) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_authz_worker: 'require claim' missing specification(s) in configuration, declining"); + return DECLINED; + } + + /* log the event, also in Apache speak */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authz_worker: authorization denied for client session"); + ap_note_auth_failure(r); + + return HTTP_UNAUTHORIZED; +} + +#if MODULE_MAGIC_NUMBER_MAJOR >= 20100714 +/* + * Apache >=2.4 authorization routine: match the claims from the authenticated user against the Require primitive + */ +authz_status oidc_authz_worker24(request_rec *r, const apr_json_value_t * const claims, const char *require_args) { + + int count_oauth_claims = 0; + const char *t, *w; + + /* needed for anonymous authentication */ + if (r->user == NULL) return AUTHZ_DENIED_NO_USER; + + /* if no claims, impossible to satisfy */ + if (!claims) return AUTHZ_DENIED; + + /* loop over the Required specifications */ + t = require_args; + while ((w = ap_getword_conf(r->pool, &t)) && w[0]) { + + count_oauth_claims++; + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authz_worker24: evaluating claim specification: %s", + w); + + /* see if we can match any of out input claims against this Require'd value */ + if (oidc_authz_match_claim(r, w, claims) == TRUE) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authz_worker24: require claim " + "'%s' matched", w); + return AUTHZ_GRANTED; + } + } + + /* if there wasn't anything after the Require claims directive... */ + if (count_oauth_claims == 0) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_authz_worker24: 'require claim' missing specification(s) in configuration, denying"); + } + + return AUTHZ_DENIED; +} +#endif diff --git a/src/cache/.gitignore b/src/cache/.gitignore new file mode 100644 index 00000000..06327e77 --- /dev/null +++ b/src/cache/.gitignore @@ -0,0 +1,4 @@ +/*.lo +/*.o +/*.slo +/.libs diff --git a/src/cache/cache.h b/src/cache/cache.h new file mode 100644 index 00000000..677001f3 --- /dev/null +++ b/src/cache/cache.h @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * mem_cache-like interface and semantics (string keys/values) using a storage backend + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#ifndef _MOD_AUTH_CONNECT_CACHE_H_ +#define _MOD_AUTH_CONNECT_CACHE_H_ + +typedef void * (*oidc_cache_cfg_create)(apr_pool_t *pool); +typedef int (*oidc_cache_post_config_function)(server_rec *s); +typedef int (*oidc_cache_child_init_function)(apr_pool_t *p, server_rec *s); +typedef apr_byte_t (*oidc_cache_get_function)(request_rec *r, const char *key, const char **value); +typedef apr_byte_t (*oidc_cache_set_function)(request_rec *r, const char *key, const char *value, apr_time_t expiry); + +typedef struct oidc_cache_t { + oidc_cache_cfg_create create_config; + oidc_cache_post_config_function post_config; + oidc_cache_child_init_function child_init; + oidc_cache_get_function get; + oidc_cache_set_function set; +} oidc_cache_t; + +extern oidc_cache_t oidc_cache_file; +extern oidc_cache_t oidc_cache_memcache; +extern oidc_cache_t oidc_cache_shm; + +#endif /* _MOD_AUTH_CONNECT_CACHE_H_ */ diff --git a/src/cache/file.c b/src/cache/file.c new file mode 100644 index 00000000..b87ba933 --- /dev/null +++ b/src/cache/file.c @@ -0,0 +1,474 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * caching using a file storage backend + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include +#include +#include + +#include +#include + +#include "../mod_auth_openidc.h" + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +/* + * header structure that holds the metadata info for a cache file entry + */ +typedef struct { + /* length of the cached data */ + apr_size_t len; + /* cache expiry timestamp */ + apr_time_t expire; +} oidc_cache_file_info_t; + +/* + * prefix that distinguishes mod_auth_openidc cache files from other files in the same directory (/tmp) + */ +#define OIDC_CACHE_FILE_PREFIX "mod-auth-connect-" + +/* post config routine */ +int oidc_cache_file_post_config(server_rec *s) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config(s->module_config, + &auth_openidc_module); + if (cfg->cache_file_dir == NULL) { + /* by default we'll use the OS specified /tmp dir for cache files */ + apr_temp_dir_get((const char **) &cfg->cache_file_dir, + s->process->pool); + } + return OK; +} + +/* + * return the cache file name for a specified key + */ +static const char *oidc_cache_file_name(request_rec *r, const char *key) { + return apr_psprintf(r->pool, "%s%s", OIDC_CACHE_FILE_PREFIX, key); +} + +/* + * return the fully qualified path name to a cache file for a specified key + */ +static const char *oidc_cache_file_path(request_rec *r, const char *key) { + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + return apr_psprintf(r->pool, "%s/%s", cfg->cache_file_dir, + oidc_cache_file_name(r, key)); +} + +/* + * read a specified number of bytes from a cache file in to a preallocated buffer + */ +static apr_status_t oidc_cache_file_read(request_rec *r, const char *path, + apr_file_t *fd, void *buf, const apr_size_t len) { + + apr_status_t rc = APR_SUCCESS; + apr_size_t bytes_read = 0; + char s_err[128]; + + /* (blocking) read the requested number of bytes */ + rc = apr_file_read_full(fd, buf, len, &bytes_read); + + /* test for system errors */ + if (rc != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_read: could not read from: %s (%s)", path, + apr_strerror(rc, s_err, sizeof(s_err))); + } + + /* ensure that we've got the requested number of bytes */ + if (bytes_read != len) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_read: could not read enough bytes from: \"%s\", bytes_read (%" APR_SIZE_T_FMT ") != len (%" APR_SIZE_T_FMT ")", + path, bytes_read, len); + rc = APR_EGENERAL; + } + + return rc; +} + +/* + * write a specified number of bytes from a buffer to a cache file + */ +static apr_status_t oidc_cache_file_write(request_rec *r, const char *path, + apr_file_t *fd, void *buf, const apr_size_t len) { + + apr_status_t rc = APR_SUCCESS; + apr_size_t bytes_written = 0; + char s_err[128]; + + /* (blocking) write the number of bytes in the buffer */ + rc = apr_file_write_full(fd, buf, len, &bytes_written); + + /* check for a system error */ + if (rc != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_write: could not write to: \"%s\" (%s)", path, + apr_strerror(rc, s_err, sizeof(s_err))); + return rc; + } + + /* check that all bytes from the header were written */ + if (bytes_written != len) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_write: could not write enough bytes to: \"%s\", bytes_written (%" APR_SIZE_T_FMT ") != len (%" APR_SIZE_T_FMT ")", + path, bytes_written, len); + return APR_EGENERAL; + } + + return rc; +} + +/* + * get a value for the specified key from the cache + */ +static apr_byte_t oidc_cache_file_get(request_rec *r, const char *key, + const char **value) { + apr_file_t *fd = NULL; + apr_status_t rc = APR_SUCCESS; + char s_err[128]; + + /* get the fully qualified path to the cache file based on the key name */ + const char *path = oidc_cache_file_path(r, key); + + /* open the cache file if it exists, otherwise we just have a "regular" cache miss */ + if (apr_file_open(&fd, path, APR_FOPEN_READ | APR_FOPEN_BUFFERED, + APR_OS_DEFAULT, r->pool) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_file_get: cache miss for key \"%s\"", key); + return TRUE; + } + + /* the file exists, now lock it */ + apr_file_lock(fd, APR_FLOCK_EXCLUSIVE); + + /* move the read pointer to the very start of the cache file */ + apr_off_t begin = 0; + apr_file_seek(fd, APR_SET, &begin); + + /* read a header with metadata */ + oidc_cache_file_info_t info; + if ((rc = oidc_cache_file_read(r, path, fd, &info, + sizeof(oidc_cache_file_info_t))) != APR_SUCCESS) + goto error_close; + + /* check if this cache entry has already expired */ + if (apr_time_now() >= info.expire) { + + /* yep, expired: unlock and close before deleting the cache file */ + apr_file_unlock(fd); + apr_file_close(fd); + + /* log this event */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_file_get: cache entry \"%s\" expired, removing file \"%s\"", + key, path); + + /* and kill it */ + if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_get: could not delete cache file \"%s\" (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + } + + /* nothing strange happened really */ + return TRUE; + } + + /* allocate space for the actual value based on the data size info in the header (+1 for \0 termination) */ + *value = apr_palloc(r->pool, info.len); + + /* (blocking) read the requested data in to the buffer */ + rc = oidc_cache_file_read(r, path, fd, (void *) *value, info.len); + + /* barf on failure */ + if (rc != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_get: could not read cache value from \"%s\"", + path); + goto error_close; + } + + /* we're done, unlock and close the file */ + apr_file_unlock(fd); + apr_file_close(fd); + + /* log a successful cache hit */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_file_get: cache hit for key \"%s\" (%" APR_SIZE_T_FMT " bytes, expiring in: %" APR_TIME_T_FMT ")", + key, info.len, apr_time_sec(info.expire - apr_time_now())); + + return TRUE; + +error_close: + + apr_file_unlock(fd); + apr_file_close(fd); + + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_get: return error status %d (%s)", rc, + apr_strerror(rc, s_err, sizeof(s_err))); + + return FALSE; +} + +// TODO: make these configurable? +#define OIDC_CACHE_FILE_LAST_CLEANED "last-cleaned" + +/* + * delete all expired entries from the cache directory + */ +static apr_status_t oidc_cache_file_clean(request_rec *r) { + apr_status_t rc = APR_SUCCESS; + apr_dir_t *dir = NULL; + apr_file_t *fd = NULL; + apr_status_t i; + apr_finfo_t fi; + oidc_cache_file_info_t info; + char s_err[128]; + + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + + /* get the path to the metadata file that holds "last cleaned" metadata info */ + const char *metadata_path = oidc_cache_file_path(r, + OIDC_CACHE_FILE_LAST_CLEANED); + + /* open the metadata file if it exists */ + if ((rc = apr_stat(&fi, metadata_path, APR_FINFO_MTIME, r->pool)) + == APR_SUCCESS) { + + /* really only clean once per so much time, check that we haven not recently run */ + if (apr_time_now() < fi.mtime + apr_time_from_sec(cfg->cache_file_clean_interval)) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_clean: last cleanup call was less than %d seconds ago (next one as early as in %" APR_TIME_T_FMT " secs)", + cfg->cache_file_clean_interval, + apr_time_sec( + fi.mtime + apr_time_from_sec(cfg->cache_file_clean_interval) - apr_time_now())); + return APR_SUCCESS; + } + + /* time to clean, reset the modification time of the metadata file to reflect the timestamp of this cleaning cycle */ + apr_file_mtime_set(metadata_path, apr_time_now(), r->pool); + + } else { + + /* no metadata file exists yet, create one (and open it) */ + if ((rc = apr_file_open(&fd, metadata_path, + (APR_FOPEN_WRITE | APR_FOPEN_CREATE), APR_OS_DEFAULT, r->pool)) + != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_clean: error creating cache timestamp file '%s' (%s)", + metadata_path, apr_strerror(rc, s_err, sizeof(s_err))); + return rc; + } + + /* and cleanup... */ + if ((rc = apr_file_close(fd)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_clean: error closing cache timestamp file '%s' (%s)", + metadata_path, apr_strerror(rc, s_err, sizeof(s_err))); + } + } + + /* time to clean, open the cache directory */ + if ((rc = apr_dir_open(&dir, cfg->cache_file_dir, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_clean: error opening cache directory '%s' for cleaning (%s)", + cfg->cache_file_dir, apr_strerror(rc, s_err, sizeof(s_err))); + return rc; + } + + /* loop trough the cache file entries */ + do { + + /* read the next entry from the directory */ + i = apr_dir_read(&fi, APR_FINFO_NAME, dir); + + if (i == APR_SUCCESS) { + + /* skip non-cache entries, cq. the ".", ".." and the metadata file */ + if ((fi.name[0] == '.') + || (strstr(fi.name, OIDC_CACHE_FILE_PREFIX) != fi.name) + || ((apr_strnatcmp(fi.name, oidc_cache_file_name(r, + OIDC_CACHE_FILE_LAST_CLEANED)) == 0))) + continue; + + /* get the fully qualified path to the cache file and open it */ + const char *path = apr_psprintf(r->pool, "%s/%s", + cfg->cache_file_dir, fi.name); + if ((rc = apr_file_open(&fd, path, APR_FOPEN_READ, APR_OS_DEFAULT, + r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_clean: unable to open cache entry \"%s\" (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + continue; + } + + /* read the header with cache metadata info */ + rc = oidc_cache_file_read(r, path, fd, &info, + sizeof(oidc_cache_file_info_t)); + apr_file_close(fd); + + if (rc == APR_SUCCESS) { + + /* check if this entry expired, if not just continue to the next entry */ + if (apr_time_now() < info.expire) + continue; + + /* the cache entry expired, we're going to remove it so log that event */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_file_clean: cache entry (%s) expired, removing file \"%s\")", + fi.name, path); + + } else { + + /* file open returned an error, log that */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_clean: cache entry (%s) corrupted (%s), removing file \"%s\"", + fi.name, apr_strerror(rc, s_err, sizeof(s_err)), path); + + } + + /* delete the cache file */ + if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) { + + /* hrm, this will most probably happen again on the next run... */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_clean: could not delete cache file \"%s\" (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + } + + } + + } while (i == APR_SUCCESS); + + apr_dir_close(dir); + + return APR_SUCCESS; +} + +/* + * write a value for the specified key to the cache + */ +static apr_byte_t oidc_cache_file_set(request_rec *r, const char *key, + const char *value, apr_time_t expiry) { + apr_file_t *fd = NULL; + apr_status_t rc = APR_SUCCESS; + char s_err[128]; + + /* get the fully qualified path to the cache file based on the key name */ + const char *path = oidc_cache_file_path(r, key); + + /* only on writes (not on reads) we clean the cache first (if not done recently) */ + oidc_cache_file_clean(r); + + /* just remove cache file if value is NULL */ + if (value == NULL) { + if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_set: could not delete cache file \"%s\" (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + } + return TRUE; + } + + /* try to open the cache file for writing, creating it if it does not exist */ + if ((rc = apr_file_open(&fd, path, (APR_FOPEN_WRITE | APR_FOPEN_CREATE), + APR_OS_DEFAULT, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_file_set: cache file \"%s\" could not be opened (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + return FALSE; + } + + /* lock the file and move the write pointer to the start of it */ + apr_file_lock(fd, APR_FLOCK_EXCLUSIVE); + apr_off_t begin = 0; + apr_file_seek(fd, APR_SET, &begin); + + /* construct the metadata for this cache entry in the header info */ + oidc_cache_file_info_t info; + info.expire = expiry; + info.len = strlen(value) + 1; + + /* write the header */ + if ((rc = oidc_cache_file_write(r, path, fd, &info, + sizeof(oidc_cache_file_info_t))) != APR_SUCCESS) + return FALSE; + + /* next write the value */ + if ((rc = oidc_cache_file_write(r, path, fd, (void *) value, info.len)) + != APR_SUCCESS) + return FALSE; + + /* unlock and close the written file */ + apr_file_unlock(fd); + apr_file_close(fd); + + /* log our success */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_file_set: set entry for key \"%s\" (%" APR_SIZE_T_FMT " bytes, expires in: %" APR_TIME_T_FMT ")", + key, info.len, apr_time_sec(expiry - apr_time_now())); + + return TRUE; +} + +oidc_cache_t oidc_cache_file = { + NULL, + oidc_cache_file_post_config, + NULL, + oidc_cache_file_get, + oidc_cache_file_set +}; diff --git a/src/cache/memcache.c b/src/cache/memcache.c new file mode 100644 index 00000000..1d60155d --- /dev/null +++ b/src/cache/memcache.c @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * caching using a memcache backend + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include "apr_general.h" +#include "apr_strings.h" +#include "apr_hash.h" +#include "apr_memcache.h" + +#include +#include +#include + +#include "../mod_auth_openidc.h" + +// TODO: proper memcache error reporting (server unreachable etc.) + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +typedef struct oidc_cache_cfg_memcache_t { + /* cache_type = memcache: memcache ptr */ + apr_memcache_t *cache_memcache; +} oidc_cache_cfg_memcache_t; + +/* create the cache context */ +static void *oidc_cache_memcache_cfg_create(apr_pool_t *pool) { + oidc_cache_cfg_memcache_t *context = apr_pcalloc(pool, sizeof(oidc_cache_cfg_memcache_t)); + context->cache_memcache = NULL; + return context; +} + +/* + * initialize the memcache struct to a number of memcache servers + */ +static int oidc_cache_memcache_post_config(server_rec *s) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + s->module_config, &auth_openidc_module); + oidc_cache_cfg_memcache_t *context = (oidc_cache_cfg_memcache_t *)cfg->cache_cfg; + + apr_status_t rv = APR_SUCCESS; + int nservers = 0; + char* split; + char* tok; + apr_pool_t *p = s->process->pool; + + if (cfg->cache_memcache_servers == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, + "oidc_cache_memcache_post_config: cache type is set to \"memcache\", but no valid OIDCMemCacheServers setting was found"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* loop over the provided memcache servers to find out the number of servers configured */ + char *cache_config = apr_pstrdup(p, cfg->cache_memcache_servers); + split = apr_strtok(cache_config, " ", &tok); + while (split) { + nservers++; + split = apr_strtok(NULL, " ", &tok); + } + + /* allocated space for the number of servers */ + rv = apr_memcache_create(p, nservers, 0, &context->cache_memcache); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_memcache_init: failed to create memcache object of '%d' size", + nservers); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* loop again over the provided servers */ + cache_config = apr_pstrdup(p, cfg->cache_memcache_servers); + split = apr_strtok(cache_config, " ", &tok); + while (split) { + apr_memcache_server_t* st; + char* host_str; + char* scope_id; + apr_port_t port; + + /* parse out host and port */ + rv = apr_parse_addr_port(&host_str, &scope_id, &port, split, p); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_memcache_init: failed to parse cache server: '%s'", + split); + return HTTP_INTERNAL_SERVER_ERROR; + } + + if (host_str == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_memcache_init: failed to parse cache server, " + "no hostname specified: '%s'", split); + return HTTP_INTERNAL_SERVER_ERROR; + } + + if (port == 0) + port = 11211; + + /* create the memcache server struct */ + // TODO: tune this + rv = apr_memcache_server_create(p, host_str, port, 0, 1, 1, 60, &st); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_memcache_init: failed to create cache server: %s:%d", + host_str, port); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* add the memcache server struct to the list */ + rv = apr_memcache_add_server(context->cache_memcache, st); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_memcache_init: failed to add cache server: %s:%d", + host_str, port); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* go to the next entry */ + split = apr_strtok(NULL, " ", &tok); + } + + return OK; +} + +/* + * get a name/value pair from memcache + */ +static apr_byte_t oidc_cache_memcache_get(request_rec *r, const char *key, + const char **value) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_memcache_get: entering \"%s\"", key); + + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + oidc_cache_cfg_memcache_t *context = (oidc_cache_cfg_memcache_t *)cfg->cache_cfg; + + apr_size_t len = 0; + + /* get it */ + apr_status_t rv = apr_memcache_getp(context->cache_memcache, r->pool, key, + (char **)value, &len, NULL); + + // TODO: error strings ? + if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, + "oidc_cache_memcache_get: apr_memcache_getp returned an error"); + return FALSE; + } + + /* do sanity checking on the string value */ + if ( (*value) && (strlen(*value) != len) ) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, + "oidc_cache_memcache_get: apr_memcache_getp returned less bytes than expected: strlen(value) [%zu] != len [%" APR_SIZE_T_FMT "]", strlen(*value), len); + return FALSE; + } + + return TRUE; +} + +/* + * store a name/value pair in memcache + */ +static apr_byte_t oidc_cache_memcache_set(request_rec *r, const char *key, + const char *value, apr_time_t expiry) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_memcache_set: entering \"%s\"", key); + + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + oidc_cache_cfg_memcache_t *context = (oidc_cache_cfg_memcache_t *)cfg->cache_cfg; + + apr_status_t rv = APR_SUCCESS; + + /* see if we should be clearing this entry */ + if (value == NULL) { + + rv = apr_memcache_delete(context->cache_memcache, key, 0); + + // TODO: error strings ? + if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, + "oidc_cache_memcache_set: apr_memcache_delete returned an error"); + } + + } else { + + /* calculate the timeout from now */ + apr_uint32_t timeout = apr_time_sec(expiry - apr_time_now()); + + /* store it */ + rv = apr_memcache_set(context->cache_memcache, key, (char *)value, + strlen(value), timeout, 0); + + // TODO: error strings ? + if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, + "oidc_cache_memcache_set: apr_memcache_set returned an error"); + } + } + + return (rv == APR_SUCCESS); +} + +oidc_cache_t oidc_cache_memcache = { + oidc_cache_memcache_cfg_create, + oidc_cache_memcache_post_config, + NULL, + oidc_cache_memcache_get, + oidc_cache_memcache_set +}; diff --git a/src/cache/shm.c b/src/cache/shm.c new file mode 100644 index 00000000..175f3b74 --- /dev/null +++ b/src/cache/shm.c @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * caching using a shared memory backend, FIFO-style + * based on mod_auth_mellon code + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include + +#include +#include +#include + +#ifdef AP_NEED_SET_MUTEX_PERMS +#include "unixd.h" +#endif + +#include "../mod_auth_openidc.h" + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +typedef struct oidc_cache_cfg_shm_t { + char *mutex_filename; + apr_shm_t *shm; + apr_global_mutex_t *mutex; +} oidc_cache_cfg_shm_t; + +/* size of key in cached key/value pairs */ +#define OIDC_CACHE_SHM_KEY_MAX 128 +/* max value size */ +#define OIDC_CACHE_SHM_VALUE_MAX 16384 + +/* represents one (fixed size) cache entry, cq. name/value string pair */ +typedef struct oidc_cache_shm_entry_t { + /* name of the cache entry */ + char key[OIDC_CACHE_SHM_KEY_MAX]; + /* value of the cache entry */ + char value[OIDC_CACHE_SHM_VALUE_MAX]; + /* last (read) access timestamp */ + apr_time_t access; + /* expiry timestamp */ + apr_time_t expires; +} oidc_cache_shm_entry_t; + +/* create the cache context */ +static void *oidc_cache_shm_cfg_create(apr_pool_t *pool) { + oidc_cache_cfg_shm_t *context = apr_pcalloc(pool, sizeof(oidc_cache_cfg_shm_t)); + context->mutex_filename = NULL; + context->shm = NULL; + context->mutex = NULL; + return context; +} + +/* + * initialized the shared memory block in the parent process + */ +int oidc_cache_shm_post_config(server_rec *s) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config(s->module_config, + &auth_openidc_module); + oidc_cache_cfg_shm_t *context = (oidc_cache_cfg_shm_t *)cfg->cache_cfg; + + /* create the shared memory segment */ + apr_status_t rv = apr_shm_create(&context->shm, + sizeof(oidc_cache_shm_entry_t) * cfg->cache_shm_size_max, + NULL, s->process->pool); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_shm_post_config: apr_shm_create failed to create shared memory segment"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* initialize the whole segment to '/0' */ + int i; + oidc_cache_shm_entry_t *table = apr_shm_baseaddr_get(context->shm); + for (i = 0; i < cfg->cache_shm_size_max; i++) { + table[i].key[0] = '\0'; + table[i].access = 0; + } + + const char *dir; + apr_temp_dir_get(&dir, s->process->pool); + /* construct the mutex filename */ + context->mutex_filename = apr_psprintf(s->process->pool, + "%s/httpd_mutex.%ld.%pp", dir, (long int) getpid(), s); + + /* create the mutex lock */ + rv = apr_global_mutex_create(&context->mutex, + (const char *) context->mutex_filename, APR_LOCK_DEFAULT, + s->process->pool); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_shm_post_config: apr_global_mutex_create failed to create mutex on file %s", + context->mutex_filename); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* need this on Linux */ +#ifdef AP_NEED_SET_MUTEX_PERMS +#if MODULE_MAGIC_NUMBER_MAJOR >= 20081201 + rv = ap_unixd_set_global_mutex_perms(context->mutex); +#else + rv = unixd_set_global_mutex_perms(context->mutex); +#endif + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "oidc_cache_shm_post_config: unixd_set_global_mutex_perms failed; could not set permissions "); + return HTTP_INTERNAL_SERVER_ERROR; + } +#endif + + return OK; +} + +/* + * initialize the shared memory segment in a child process + */ +int oidc_cache_shm_child_init(apr_pool_t *p, server_rec *s) { + oidc_cfg *cfg = ap_get_module_config(s->module_config, &auth_openidc_module); + oidc_cache_cfg_shm_t *context = (oidc_cache_cfg_shm_t *)cfg->cache_cfg; + + /* initialize the lock for the child process */ + apr_status_t rv = apr_global_mutex_child_init(&context->mutex, + (const char *) context->mutex_filename, p); + + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, + "oic_cache_shm_child_init: apr_global_mutex_child_init failed to reopen mutex on file %s", + context->mutex_filename); + } + + return rv; +} + +/* + * get a value from the shared memory cache + */ +static apr_byte_t oidc_cache_shm_get(request_rec *r, const char *key, + const char **value) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_shm_get: entering \"%s\"", key); + + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + oidc_cache_cfg_shm_t *context = (oidc_cache_cfg_shm_t *)cfg->cache_cfg; + + apr_status_t rv; + int i; + *value = NULL; + + /* grab the global lock */ + if ((rv = apr_global_mutex_lock(context->mutex)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, + "oidc_cache_shm_get: apr_global_mutex_lock() failed [%d]", rv); + return FALSE; + } + + /* get the pointer to the start of the shared memory block */ + oidc_cache_shm_entry_t *table = apr_shm_baseaddr_get(context->shm); + + /* loop over the block, looking for the key */ + for (i = 0; i < cfg->cache_shm_size_max; i++) { + const char *tablekey = table[i].key; + + if (tablekey == NULL) + continue; + + if (strcmp(tablekey, key) == 0) { + + /* found a match, check if it has expired */ + if (table[i].expires > apr_time_now()) { + + /* update access timestamp */ + table[i].access = apr_time_now(); + *value = table[i].value; + } + } + } + + /* release the global lock */ + apr_global_mutex_unlock(context->mutex); + + return (*value == NULL) ? FALSE : TRUE; +} + +/* + * store a value in the shared memory cache + */ +static apr_byte_t oidc_cache_shm_set(request_rec *r, const char *key, + const char *value, apr_time_t expiry) { + + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + oidc_cache_cfg_shm_t *context = (oidc_cache_cfg_shm_t *)cfg->cache_cfg; + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_cache_shm_set: entering \"%s\" (value size=(%zu)", key, + value ? strlen(value) : 0); + + oidc_cache_shm_entry_t *match, *free, *lru; + oidc_cache_shm_entry_t *table; + apr_time_t current_time; + int i; + apr_time_t age; + + /* check that the passed in key is valid */ + if (key == NULL || strlen(key) > OIDC_CACHE_SHM_KEY_MAX) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_shm_set: could not set value since key is NULL or too long (%s)", + key); + return FALSE; + } + + /* check that the passed in value is valid */ + if ( (value != NULL) && strlen(value) > OIDC_CACHE_SHM_VALUE_MAX) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_shm_set: could not set value since value is too long (%zu > %d)", + strlen(value), OIDC_CACHE_SHM_VALUE_MAX); + return FALSE; + } + + /* grab the global lock */ + if (apr_global_mutex_lock(context->mutex) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_cache_shm_set: apr_global_mutex_lock() failed"); + return FALSE; + } + + /* get a pointer to the shared memory block */ + table = apr_shm_baseaddr_get(context->shm); + + /* get the current time */ + current_time = apr_time_now(); + + /* loop over the block, looking for the key */ + match = NULL; + free = NULL; + lru = &table[0]; + for (i = 0; i < cfg->cache_shm_size_max; i++) { + + /* see if this slot is free */ + if (table[i].key[0] == '\0') { + if (free == NULL) free = &table[i]; + continue; + } + + /* see if a value already exists for this key */ + if (strcmp(table[i].key, key) == 0) { + match = &table[i]; + break; + } + + /* see if this slot has expired */ + if (table[i].expires <= current_time) { + if (free == NULL) free = &table[i]; + continue; + } + + /* see if this slot was less recently used than the current pointer */ + if (table[i].access < lru->access) { + lru = &table[i]; + } + + } + + /* if we have no free slots, issue a warning about the LRU entry */ + if (match == NULL && free == NULL) { + age = (current_time - lru->access) / 1000000; + if (age < 3600) { + ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, + "oidc_cache_shm_set: dropping LRU entry with age = %" APR_TIME_T_FMT "s, which is less than one hour; consider increasing the shared memory caching space (which is %d now) with the (global) OIDCCacheShmMax setting.", + age, cfg->cache_shm_size_max); + } + } + + oidc_cache_shm_entry_t *t = match ? match : (free ? free : lru); + + if (value != NULL) { + + /* fill out the entry with the provided data */ + strcpy(t->key, key); + strcpy(t->value, value); + t->expires = expiry; + t->access = current_time; + + } else { + + t->key[0] = '\0'; + } + + /* release the global lock */ + apr_global_mutex_unlock(context->mutex); + + return TRUE; +} + +oidc_cache_t oidc_cache_shm = { + oidc_cache_shm_cfg_create, + oidc_cache_shm_post_config, + oidc_cache_shm_child_init, + oidc_cache_shm_get, + oidc_cache_shm_set +}; diff --git a/src/config.c b/src/config.c new file mode 100644 index 00000000..67bb44d7 --- /dev/null +++ b/src/config.c @@ -0,0 +1,1093 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "mod_auth_openidc.h" + +/* validate SSL server certificates by default */ +#define OIDC_DEFAULT_SSL_VALIDATE_SERVER 1 +/* default token endpoint authentication method */ +#define OIDC_DEFAULT_ENDPOINT_AUTH "client_secret_basic" +/* default scope requested from the OP */ +#define OIDC_DEFAULT_SCOPE "openid" +/* default claim delimiter for multi-valued claims passed in a HTTP header */ +#define OIDC_DEFAULT_CLAIM_DELIMITER "," +/* default prefix for claim names being passed in HTTP headers */ +#define OIDC_DEFAULT_CLAIM_PREFIX "OIDC_CLAIM_" +/* default name of the session cookie */ +#define OIDC_DEFAULT_COOKIE "mod_auth_openidc_session" +/* default for the HTTP header name in which the remote user name is passed */ +#define OIDC_DEFAULT_AUTHN_HEADER NULL +/* scrub HTTP headers by default unless overridden (and insecure) */ +#define OIDC_DEFAULT_SCRUB_REQUEST_HEADERS 1 +/* default client_name the client uses for dynamic client registration */ +#define OIDC_DEFAULT_CLIENT_NAME "OpenID Connect Apache Module (mod_auth_openidc)" +/* timeouts in seconds for HTTP calls that may take a long time */ +#define OIDC_DEFAULT_HTTP_TIMEOUT_LONG 60 +/* timeouts in seconds for HTTP calls that should take a short time (registry/discovery related) */ +#define OIDC_DEFAULT_HTTP_TIMEOUT_SHORT 5 +/* default session storage type */ +#define OIDC_DEFAULT_SESSION_TYPE OIDC_SESSION_TYPE_22_CACHE_FILE +/* timeout in seconds after which state expires */ +#define OIDC_DEFAULT_STATE_TIMEOUT 300 +/* default OpenID Connect authorization response type */ +#define OIDC_DEFAULT_RESPONSE_TYPE "code" +/* default duration in seconds after which retrieved JWS should be refreshed */ +#define OIDC_DEFAULT_JWKS_REFRESH_INTERVAL 3600 +/* default max cache size for shm */ +#define OIDC_DEFAULT_CACHE_SHM_SIZE 500 +/* for issued-at timestamp (iat) checking */ +#define OIDC_DEFAULT_IDTOKEN_IAT_SLACK 600 +/* for file-based caching: clean interval in seconds */ +#define OIDC_DEFAULT_CACHE_FILE_CLEAN_INTERVAL 60 + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +/* + * set a boolean value in the server config + */ +const char *oidc_set_flag_slot(cmd_parms *cmd, void *struct_ptr, int arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + return ap_set_flag_slot(cmd, cfg, arg); +} + +/* + * set a string value in the server config + */ +const char *oidc_set_string_slot(cmd_parms *cmd, void *struct_ptr, + const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + return ap_set_string_slot(cmd, cfg, arg); +} + +/* + * set an integer value in the server config + */ +const char *oidc_set_int_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + return ap_set_int_slot(cmd, cfg, arg); +} + +/* + * set a URL value in the server config + */ +static const char *oidc_set_url_slot_type(cmd_parms *cmd, void *ptr, + const char *arg, const char *type) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + apr_uri_t url; + if (apr_uri_parse(cmd->pool, arg, &url) != APR_SUCCESS) { + return apr_psprintf(cmd->pool, + "oidc_set_url_slot_type: configuration value '%s' could not be parsed as a URL!", + arg); + } + + if (url.scheme == NULL) { + return apr_psprintf(cmd->pool, + "oidc_set_url_slot_type: configuration value '%s' could not be parsed as a URL (no scheme set)!", + arg); + } + + if (type == NULL) { + if ((strcmp(url.scheme, "http") != 0) + && (strcmp(url.scheme, "https") != 0)) { + return apr_psprintf(cmd->pool, + "oidc_set_url_slot_type: configuration value '%s' could not be parsed as a HTTP/HTTPs URL (scheme != http/https)!", + arg); + } + } else if (strcmp(url.scheme, type) != 0) { + return apr_psprintf(cmd->pool, + "oidc_set_url_slot_type: configuration value '%s' could not be parsed as a \"%s\" URL (scheme == %s != \"%s\")!", + arg, type, url.scheme, type); + } + + if (url.hostname == NULL) { + return apr_psprintf(cmd->pool, + "oidc_set_url_slot_type: configuration value '%s' could not be parsed as a HTTP/HTTPs URL (no hostname set, check your slashes)!", + arg); + } + return ap_set_string_slot(cmd, cfg, arg); +} + +/* + * set a HTTPS value in the server config + */ +const char *oidc_set_https_slot(cmd_parms *cmd, void *ptr, const char *arg) { + return oidc_set_url_slot_type(cmd, ptr, arg, "https"); +} + +/* + * set a HTTPS/HTTP value in the server config + */ +const char *oidc_set_url_slot(cmd_parms *cmd, void *ptr, const char *arg) { + return oidc_set_url_slot_type(cmd, ptr, arg, NULL); +} + +/* + * set a directory value in the server config + */ +// TODO: it's not really a syntax error... (could be fixed at runtime but then we'd have to restart the server) +const char *oidc_set_dir_slot(cmd_parms *cmd, void *ptr, const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + + char s_err[128]; + apr_dir_t *dir; + apr_status_t rc = APR_SUCCESS; + + /* ensure the directory exists */ + if ((rc = apr_dir_open(&dir, arg, cmd->pool)) != APR_SUCCESS) { + return apr_psprintf(cmd->pool, + "oidc_set_dir_slot: could not access directory '%s' (%s)", arg, + apr_strerror(rc, s_err, sizeof(s_err))); + } + + /* and cleanup... */ + if ((rc = apr_dir_close(dir)) != APR_SUCCESS) { + return apr_psprintf(cmd->pool, + "oidc_set_dir_slot: could not close directory '%s' (%s)", arg, + apr_strerror(rc, s_err, sizeof(s_err))); + } + + return ap_set_string_slot(cmd, cfg, arg); +} + +/* + * set the cookie domain in the server config and check it syntactically + */ +const char *oidc_set_cookie_domain(cmd_parms *cmd, void *ptr, const char *value) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + size_t sz, limit; + char d; + limit = strlen(value); + for (sz = 0; sz < limit; sz++) { + d = value[sz]; + if ((d < '0' || d > '9') && (d < 'a' || d > 'z') && (d < 'A' || d > 'Z') + && d != '.' && d != '-') { + return (apr_psprintf(cmd->pool, + "oidc_set_cookie_domain: invalid character (%c) in OIDCCookieDomain", + d)); + } + } + cfg->cookie_domain = apr_pstrdup(cmd->pool, value); + return NULL; +} + +/* + * set the session storage type + */ +const char *oidc_set_session_type(cmd_parms *cmd, void *ptr, const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + + if (strcmp(arg, "file") == 0) { + cfg->session_type = OIDC_SESSION_TYPE_22_CACHE_FILE; + } else if (strcmp(arg, "cookie") == 0) { + cfg->session_type = OIDC_SESSION_TYPE_22_COOKIE; + } else { + return (apr_psprintf(cmd->pool, + "oidc_set_session_type: invalid value for OIDCSessionType (%s); must be one of \"file\" or \"cookie\"", + arg)); + } + + return NULL; +} + +/* + * set the cache type + */ +const char *oidc_set_cache_type(cmd_parms *cmd, void *ptr, const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + + if (strcmp(arg, "file") == 0) { + cfg->cache = &oidc_cache_file; + } else if (strcmp(arg, "memcache") == 0) { + cfg->cache = &oidc_cache_memcache; + } else if (strcmp(arg, "shm") == 0) { + cfg->cache = &oidc_cache_shm; + } else { + return (apr_psprintf(cmd->pool, + "oidc_set_cache_type: invalid value for OIDCCacheType (%s); must be one of \"file\", \"memcache\" or \"shm\"", + arg)); + } + + cfg->cache_cfg = cfg->cache->create_config ? cfg->cache->create_config(cmd->server->process->pool) : NULL; + + return NULL; +} + +/* + * set an authentication method for an endpoint and check it is one that we support + */ +const char *oidc_set_endpoint_auth_slot(cmd_parms *cmd, void *struct_ptr, + const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + + if ((apr_strnatcmp(arg, "client_secret_post") == 0) + || (apr_strnatcmp(arg, "client_secret_basic") == 0)) { + + return ap_set_string_slot(cmd, cfg, arg); + } + return "parameter must be 'client_secret_post' or 'client_secret_basic'"; +} + +/* + * set the response type used + */ +const char *oidc_set_response_type(cmd_parms *cmd, void *struct_ptr, + const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + + if ((apr_strnatcmp(arg, "code") == 0) + || (apr_strnatcmp(arg, "id_token") == 0) + || (apr_strnatcmp(arg, "id_token token") == 0) + || (apr_strnatcmp(arg, "token id_token") == 0)) { + + return ap_set_string_slot(cmd, cfg, arg); + } + return "parameter must be one of 'code', 'id_token', 'id_token token' or 'token id_token'"; +} + +/* + * set the id_token signing algorithm to be used by the OP + * TODO: align supported functions with oidc_crypto_jwt_alg2padding and metadata_is_valid function + */ +const char *oidc_set_id_token_alg(cmd_parms *cmd, void *struct_ptr, + const char *arg) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config( + cmd->server->module_config, &auth_openidc_module); + + if ((apr_strnatcmp(arg, "RS256") == 0) || (apr_strnatcmp(arg, "RS384") == 0) + || (apr_strnatcmp(arg, "RS512") == 0) + || (apr_strnatcmp(arg, "PS256") == 0) + || (apr_strnatcmp(arg, "PS384") == 0) + || (apr_strnatcmp(arg, "PS512") == 0) + || (apr_strnatcmp(arg, "HS256") == 0) + || (apr_strnatcmp(arg, "HS384") == 0) + || (apr_strnatcmp(arg, "HS512") == 0)) { + + return ap_set_string_slot(cmd, cfg, arg); + } + return "parameter must be one of 'RS256', 'RS384', 'RS512', 'HS256', 'HS384', 'HS512', 'PS256', 'PS384' or 'PS512'"; +} + +/* + * get the current path from the request in a normalized way + */ +static char *oidc_get_path(request_rec *r) { + size_t i; + char *p; + p = r->parsed_uri.path; + if (p[0] == '\0') + return apr_pstrdup(r->pool, "/"); + for (i = strlen(p) - 1; i > 0; i--) + if (p[i] == '/') + break; + return apr_pstrndup(r->pool, p, i + 1); +} + +/* + * get the cookie path setting and check that it matches the request path; cook it up if it is not set + */ +char *oidc_get_cookie_path(request_rec *r) { + char *rv = NULL, *requestPath = oidc_get_path(r); + oidc_dir_cfg *d = ap_get_module_config(r->per_dir_config, &auth_openidc_module); + if (d->cookie_path != NULL) { + if (strncmp(d->cookie_path, requestPath, strlen(d->cookie_path)) == 0) + rv = d->cookie_path; + else { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_get_cookie_path: OIDCCookiePath (%s) not a substring of request path, using request path (%s) for cookie", + d->cookie_path, requestPath); + rv = requestPath; + } + } else { + rv = requestPath; + } + return (rv); +} + +/* + * create a new server config record with defaults + */ +void *oidc_create_server_config(apr_pool_t *pool, server_rec *svr) { + oidc_cfg *c = apr_pcalloc(pool, sizeof(oidc_cfg)); + + c->merged = FALSE; + + c->redirect_uri = NULL; + c->discover_url = NULL; + c->id_token_alg = NULL; + + c->provider.issuer = NULL; + c->provider.authorization_endpoint_url = NULL; + c->provider.token_endpoint_url = NULL; + c->provider.token_endpoint_auth = OIDC_DEFAULT_ENDPOINT_AUTH; + c->provider.userinfo_endpoint_url = NULL; + c->provider.client_id = NULL; + c->provider.client_secret = NULL; + + c->provider.ssl_validate_server = OIDC_DEFAULT_SSL_VALIDATE_SERVER; + c->provider.client_name = OIDC_DEFAULT_CLIENT_NAME; + c->provider.client_contact = NULL; + c->provider.scope = OIDC_DEFAULT_SCOPE; + c->provider.response_type = OIDC_DEFAULT_RESPONSE_TYPE; + c->provider.jwks_refresh_interval = OIDC_DEFAULT_JWKS_REFRESH_INTERVAL; + c->provider.idtoken_iat_slack = OIDC_DEFAULT_IDTOKEN_IAT_SLACK; + + c->oauth.ssl_validate_server = OIDC_DEFAULT_SSL_VALIDATE_SERVER; + c->oauth.client_id = NULL; + c->oauth.client_secret = NULL; + c->oauth.validate_endpoint_url = NULL; + c->oauth.validate_endpoint_auth = OIDC_DEFAULT_ENDPOINT_AUTH; + + c->cache = &oidc_cache_file; + c->cache_cfg = c->cache->create_config? c->cache->create_config(pool) : NULL; + c->cache_file_dir = NULL; + c->cache_file_clean_interval = OIDC_DEFAULT_CACHE_FILE_CLEAN_INTERVAL; + c->cache_memcache_servers = NULL; + c->cache_shm_size_max = OIDC_DEFAULT_CACHE_SHM_SIZE; + + c->metadata_dir = NULL; + c->session_type = OIDC_DEFAULT_SESSION_TYPE; + + c->http_timeout_long = OIDC_DEFAULT_HTTP_TIMEOUT_LONG; + c->http_timeout_short = OIDC_DEFAULT_HTTP_TIMEOUT_SHORT; + c->state_timeout = OIDC_DEFAULT_STATE_TIMEOUT; + + c->cookie_domain = NULL; + c->claim_delimiter = OIDC_DEFAULT_CLAIM_DELIMITER; + c->claim_prefix = OIDC_DEFAULT_CLAIM_PREFIX; + c->crypto_passphrase = NULL; + + c->scrub_request_headers = OIDC_DEFAULT_SCRUB_REQUEST_HEADERS; + + return c; +} + +/* + * merge a new server config with a base one + */ +void *oidc_merge_server_config(apr_pool_t *pool, void *BASE, void *ADD) { + oidc_cfg *c = apr_pcalloc(pool, sizeof(oidc_cfg)); + oidc_cfg *base = BASE; + oidc_cfg *add = ADD; + + c->merged = TRUE; + + c->redirect_uri = + add->redirect_uri != NULL ? add->redirect_uri : base->redirect_uri; + c->discover_url = + add->discover_url != NULL ? add->discover_url : base->discover_url; + c->id_token_alg = + add->id_token_alg != NULL ? add->id_token_alg : base->id_token_alg; + + c->provider.issuer = + add->provider.issuer != NULL ? + add->provider.issuer : base->provider.issuer; + c->provider.authorization_endpoint_url = + add->provider.authorization_endpoint_url != NULL ? + add->provider.authorization_endpoint_url : + base->provider.authorization_endpoint_url; + c->provider.token_endpoint_url = + add->provider.token_endpoint_url != NULL ? + add->provider.token_endpoint_url : + base->provider.token_endpoint_url; + c->provider.token_endpoint_auth = + strcmp(add->provider.token_endpoint_auth, + OIDC_DEFAULT_ENDPOINT_AUTH) != 0 ? + add->provider.token_endpoint_auth : + base->provider.token_endpoint_auth; + c->provider.userinfo_endpoint_url = + add->provider.userinfo_endpoint_url != NULL ? + add->provider.userinfo_endpoint_url : + base->provider.userinfo_endpoint_url; + c->provider.client_id = + add->provider.client_id != NULL ? + add->provider.client_id : base->provider.client_id; + c->provider.client_secret = + add->provider.client_secret != NULL ? + add->provider.client_secret : base->provider.client_secret; + + c->provider.ssl_validate_server = + add->provider.ssl_validate_server + != OIDC_DEFAULT_SSL_VALIDATE_SERVER ? + add->provider.ssl_validate_server : + base->provider.ssl_validate_server; + c->provider.client_name = + strcmp(add->provider.client_name, OIDC_DEFAULT_CLIENT_NAME) != 0 ? + add->provider.client_name : base->provider.client_name; + c->provider.client_contact = + add->provider.client_contact != NULL ? + add->provider.client_contact : + base->provider.client_contact; + c->provider.scope = + strcmp(add->provider.scope, OIDC_DEFAULT_SCOPE) != 0 ? + add->provider.scope : base->provider.scope; + c->provider.response_type = + strcmp(add->provider.response_type, OIDC_DEFAULT_RESPONSE_TYPE) + != 0 ? + add->provider.response_type : base->provider.response_type; + c->provider.jwks_refresh_interval = + add->provider.jwks_refresh_interval + != OIDC_DEFAULT_JWKS_REFRESH_INTERVAL ? + add->provider.jwks_refresh_interval : + base->provider.jwks_refresh_interval; + c->provider.idtoken_iat_slack = + add->provider.idtoken_iat_slack != OIDC_DEFAULT_IDTOKEN_IAT_SLACK ? + add->provider.idtoken_iat_slack : + base->provider.idtoken_iat_slack; + + + c->oauth.ssl_validate_server = + add->oauth.ssl_validate_server != OIDC_DEFAULT_SSL_VALIDATE_SERVER ? + add->oauth.ssl_validate_server : + base->oauth.ssl_validate_server; + c->oauth.client_id = + add->oauth.client_id != NULL ? + add->oauth.client_id : base->oauth.client_id; + c->oauth.client_secret = + add->oauth.client_secret != NULL ? + add->oauth.client_secret : base->oauth.client_secret; + c->oauth.validate_endpoint_url = + add->oauth.validate_endpoint_url != NULL ? + add->oauth.validate_endpoint_url : + base->oauth.validate_endpoint_url; + c->oauth.validate_endpoint_auth = + strcmp(add->oauth.validate_endpoint_auth, + OIDC_DEFAULT_ENDPOINT_AUTH) != 0 ? + add->oauth.validate_endpoint_auth : + base->oauth.validate_endpoint_auth; + + c->http_timeout_long = + add->http_timeout_long != OIDC_DEFAULT_HTTP_TIMEOUT_LONG ? + add->http_timeout_long : base->http_timeout_long; + c->http_timeout_short = + add->http_timeout_short != OIDC_DEFAULT_HTTP_TIMEOUT_SHORT ? + add->http_timeout_short : base->http_timeout_short; + c->state_timeout = + add->state_timeout != OIDC_DEFAULT_STATE_TIMEOUT ? + add->state_timeout : base->state_timeout; + + if (add->cache != &oidc_cache_file) { + c->cache = add->cache; + c->cache_cfg = add->cache_cfg; + } else { + c->cache = base->cache; + c->cache_cfg = base->cache_cfg; + } + + c->cache_file_dir = + add->cache_file_dir != NULL ? + add->cache_file_dir : base->cache_file_dir; + c->cache_file_clean_interval = + add->cache_file_clean_interval + != OIDC_DEFAULT_CACHE_FILE_CLEAN_INTERVAL ? + add->cache_file_clean_interval : + base->cache_file_clean_interval; + + c->cache_memcache_servers = + add->cache_memcache_servers != NULL ? + add->cache_memcache_servers : base->cache_memcache_servers; + c->cache_shm_size_max = + add->cache_shm_size_max != OIDC_DEFAULT_CACHE_SHM_SIZE ? + add->cache_shm_size_max : base->cache_shm_size_max; + + c->metadata_dir = + add->metadata_dir != NULL ? add->metadata_dir : base->metadata_dir; + c->session_type = + add->session_type != OIDC_DEFAULT_SESSION_TYPE ? + add->session_type : base->session_type; + + c->cookie_domain = + add->cookie_domain != NULL ? + add->cookie_domain : base->cookie_domain; + c->claim_delimiter = + strcmp(add->claim_delimiter, OIDC_DEFAULT_CLAIM_DELIMITER) != 0 ? + add->claim_delimiter : base->claim_delimiter; + c->claim_prefix = + strcmp(add->claim_prefix, OIDC_DEFAULT_CLAIM_PREFIX) != 0 ? + add->claim_prefix : base->claim_prefix; + c->crypto_passphrase = + add->crypto_passphrase != NULL ? + add->crypto_passphrase : base->crypto_passphrase; + + c->scrub_request_headers = + add->scrub_request_headers != OIDC_DEFAULT_SCRUB_REQUEST_HEADERS ? + add->scrub_request_headers : base->scrub_request_headers; + + return c; +} + +/* + * create a new directory config record with defaults + */ +void *oidc_create_dir_config(apr_pool_t *pool, char *path) { + oidc_dir_cfg *c = apr_pcalloc(pool, sizeof(oidc_dir_cfg)); + c->cookie = OIDC_DEFAULT_COOKIE; + c->cookie_path = NULL; + c->authn_header = OIDC_DEFAULT_AUTHN_HEADER; + return (c); +} + +/* + * merge a new directory config with a base one + */ +void *oidc_merge_dir_config(apr_pool_t *pool, void *BASE, void *ADD) { + oidc_dir_cfg *c = apr_pcalloc(pool, sizeof(oidc_dir_cfg)); + oidc_dir_cfg *base = BASE; + oidc_dir_cfg *add = ADD; + c->cookie = ( + apr_strnatcasecmp(add->cookie, OIDC_DEFAULT_COOKIE) != 0 ? + add->cookie : base->cookie); + c->cookie_path = ( + add->cookie_path != NULL ? add->cookie_path : base->cookie_path); + c->authn_header = ( + add->authn_header != OIDC_DEFAULT_AUTHN_HEADER ? + add->authn_header : base->authn_header); + return (c); +} + +/* + * report a config error + */ +static int oidc_check_config_error(server_rec *s, const char *config_str) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, + "oidc_check_config_error: mandatory parameter '%s' is not set", + config_str); + return HTTP_INTERNAL_SERVER_ERROR; +} + +/* + * check the config required for the OpenID Connect RP role + */ +static int oidc_check_config_openid_openidc(server_rec *s, oidc_cfg *c) { + + if ((c->metadata_dir == NULL) && (c->provider.issuer == NULL)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, + "oidc_check_config_openid_openidc: one of 'OIDCProviderIssuer' or 'OIDCMetadataDir' must be set"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + if (c->redirect_uri == NULL) + return oidc_check_config_error(s, "OIDCRedirectURI"); + if (c->crypto_passphrase == NULL) + return oidc_check_config_error(s, "OIDCCryptoPassphrase"); + + if (c->metadata_dir == NULL) { + if (c->provider.issuer == NULL) + return oidc_check_config_error(s, "OIDCProviderIssuer"); + if (c->provider.authorization_endpoint_url == NULL) + return oidc_check_config_error(s, + "OIDCProviderAuthorizationEndpoint"); + // TODO: this depends on the configured OIDCResponseType now + if (c->provider.token_endpoint_url == NULL) + return oidc_check_config_error(s, "OIDCProviderTokenEndpoint"); + if (c->provider.client_id == NULL) + return oidc_check_config_error(s, "OIDCClientID"); + // TODO: this depends on the configured OIDCResponseType now + if (c->provider.client_secret == NULL) + return oidc_check_config_error(s, "OIDCClientSecret"); + } + + return OK; +} + +/* + * check the config required for the OAuth 2.0 RS role + */ +static int oidc_check_config_oauth(server_rec *s, oidc_cfg *c) { + + if (c->oauth.client_id == NULL) + return oidc_check_config_error(s, "OIDCOAuthClientID"); + + if (c->oauth.client_secret == NULL) + return oidc_check_config_error(s, "OIDCOAuthClientSecret"); + + if (c->oauth.validate_endpoint_url == NULL) + return oidc_check_config_error(s, "OIDCOAuthEndpoint"); + + return OK; +} + +/* + * check the config of a vhost + */ +static int oidc_config_check_vhost_config(apr_pool_t *pool, server_rec *s) { + oidc_cfg *cfg = ap_get_module_config(s->module_config, &auth_openidc_module); + + ap_log_error(APLOG_MARK, OIDC_DEBUG, 0, s, + "oidc_config_check_vhost_config: entering"); + + if ((cfg->metadata_dir != NULL) || (cfg->provider.issuer == NULL) + || (cfg->redirect_uri != NULL) + || (cfg->crypto_passphrase != NULL)) { + if (oidc_check_config_openid_openidc(s, cfg) != OK) + return HTTP_INTERNAL_SERVER_ERROR; + } + + if ((cfg->oauth.client_id != NULL) || (cfg->oauth.client_secret != NULL) + || (cfg->oauth.validate_endpoint_url != NULL)) { + if (oidc_check_config_oauth(s, cfg) != OK) + return HTTP_INTERNAL_SERVER_ERROR; + } + + return OK; +} + +/* + * check the config of a merged vhost + */ +static int oidc_config_check_merged_vhost_configs(apr_pool_t *pool, + server_rec *s) { + int status = OK; + while (s != NULL && status == OK) { + oidc_cfg *cfg = ap_get_module_config(s->module_config, &auth_openidc_module); + if (cfg->merged) { + status = oidc_config_check_vhost_config(pool, s); + } + s = s->next; + } + return status; +} + +/* + * check if any merged vhost configs exist + */ +static int oidc_config_merged_vhost_configs_exist(server_rec *s) { + while (s != NULL) { + oidc_cfg *cfg = ap_get_module_config(s->module_config, &auth_openidc_module); + if (cfg->merged) { + return TRUE; + } + s = s->next; + } + return FALSE; +} + +/* + * SSL initialization magic copied from mod_auth_cas + */ +#if defined(OPENSSL_THREADS) && APR_HAS_THREADS + +static apr_thread_mutex_t **ssl_locks; +static int ssl_num_locks; + +static void oidc_ssl_locking_callback(int mode, int type, const char *file, + int line) { + if (type < ssl_num_locks) { + if (mode & CRYPTO_LOCK) + apr_thread_mutex_lock(ssl_locks[type]); + else + apr_thread_mutex_unlock(ssl_locks[type]); + } +} + +#ifdef OPENSSL_NO_THREADID +static unsigned long oidc_ssl_id_callback(void) { + return (unsigned long) apr_os_thread_current(); +} +#else +static void oidc_ssl_id_callback(CRYPTO_THREADID *id) { + CRYPTO_THREADID_set_numeric(id, (unsigned long) apr_os_thread_current()); +} +#endif /* OPENSSL_NO_THREADID */ + +#endif /* defined(OPENSSL_THREADS) && APR_HAS_THREADS */ + +apr_status_t oidc_cleanup(void *data) { +#if (defined (OPENSSL_THREADS) && APR_HAS_THREADS) + if (CRYPTO_get_locking_callback() == oidc_ssl_locking_callback) + CRYPTO_set_locking_callback(NULL); +#ifdef OPENSSL_NO_THREADID + if (CRYPTO_get_id_callback() == oidc_ssl_id_callback) + CRYPTO_set_id_callback(NULL); +#else + if (CRYPTO_THREADID_get_callback() == oidc_ssl_id_callback) + CRYPTO_THREADID_set_callback(NULL); +#endif /* OPENSSL_NO_THREADID */ + +#endif /* defined(OPENSSL_THREADS) && APR_HAS_THREADS */ + curl_global_cleanup(); + return APR_SUCCESS; +} + +/* + * handler that is called (twice) after the configuration phase; check if everything is OK + */ +int oidc_post_config(apr_pool_t *pool, apr_pool_t *p1, apr_pool_t *p2, + server_rec *s) { + const char *userdata_key = "oidc_post_config"; + void *data = NULL; + int i; + + /* Since the post_config hook is invoked twice (once + * for 'sanity checking' of the config and once for + * the actual server launch, we have to use a hack + * to not run twice + */ + apr_pool_userdata_get(&data, userdata_key, s->process->pool); + if (data == NULL) { + apr_pool_userdata_set((const void *) 1, userdata_key, + apr_pool_cleanup_null, s->process->pool); + return OK; + } + + curl_global_init(CURL_GLOBAL_ALL); + +#if (defined(OPENSSL_THREADS) && APR_HAS_THREADS) + ssl_num_locks = CRYPTO_num_locks(); + ssl_locks = apr_pcalloc(s->process->pool, + ssl_num_locks * sizeof(*ssl_locks)); + + for (i = 0; i < ssl_num_locks; i++) + apr_thread_mutex_create(&(ssl_locks[i]), APR_THREAD_MUTEX_DEFAULT, + s->process->pool); + +#ifdef OPENSSL_NO_THREADID + if (CRYPTO_get_locking_callback() == NULL && CRYPTO_get_id_callback() == NULL) { + CRYPTO_set_locking_callback(oidc_ssl_locking_callback); + CRYPTO_set_id_callback(oidc_ssl_id_callback); + } +#else + if (CRYPTO_get_locking_callback() == NULL + && CRYPTO_THREADID_get_callback() == NULL) { + CRYPTO_set_locking_callback(oidc_ssl_locking_callback); + CRYPTO_THREADID_set_callback(oidc_ssl_id_callback); + } +#endif /* OPENSSL_NO_THREADID */ +#endif /* defined(OPENSSL_THREADS) && APR_HAS_THREADS */ + apr_pool_cleanup_register(pool, s, oidc_cleanup, apr_pool_cleanup_null); + + oidc_session_init(); + + server_rec *sp = s; + while (sp != NULL) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config(sp->module_config, + &auth_openidc_module); + if (cfg->cache->post_config != NULL) { + if (cfg->cache->post_config(sp) != OK) return HTTP_INTERNAL_SERVER_ERROR; + } + sp = sp->next; + } + + /* + * Apache has a base vhost that true vhosts derive from. + * There are two startup scenarios: + * + * 1. Only the base vhost contains OIDC settings. + * No server configs have been merged. + * Only the base vhost needs to be checked. + * + * 2. The base vhost contains zero or more OIDC settings. + * One or more vhosts override these. + * These vhosts have a merged config. + * All merged configs need to be checked. + */ + if (!oidc_config_merged_vhost_configs_exist(s)) { + /* nothing merged, only check the base vhost */ + return oidc_config_check_vhost_config(pool, s); + } + return oidc_config_check_merged_vhost_configs(pool, s); +} + +#if MODULE_MAGIC_NUMBER_MAJOR >= 20100714 +static const authz_provider authz_oidc_provider = { + &oidc_authz_checker, + NULL, +}; +#endif + +/* + * initialize cache context in child process if required + */ +void oidc_child_init(apr_pool_t *p, server_rec *s) { + while (s != NULL) { + oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config(s->module_config, + &auth_openidc_module); + if (cfg->cache->child_init != NULL) { + if (cfg->cache->child_init(p, s) != APR_SUCCESS) { + // TODO: ehrm... + exit(-1); + } + } + s = s->next; + } +} + +/* + * register our authentication and authorization functions + */ +void oidc_register_hooks(apr_pool_t *pool) { + ap_hook_post_config(oidc_post_config, NULL, NULL, APR_HOOK_LAST); + ap_hook_child_init(oidc_child_init, NULL, NULL, APR_HOOK_MIDDLE); +#if MODULE_MAGIC_NUMBER_MAJOR >= 20100714 + ap_hook_check_authn(oidc_check_user_id, NULL, NULL, APR_HOOK_MIDDLE, AP_AUTH_INTERNAL_PER_CONF); + ap_register_auth_provider(pool, AUTHZ_PROVIDER_GROUP, "attribute", "0", &authz_oidc_provider, AP_AUTH_INTERNAL_PER_CONF); +#else + static const char * const authzSucc[] = { "mod_authz_user.c", NULL }; + ap_hook_check_user_id(oidc_check_user_id, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_auth_checker(oidc_auth_checker, NULL, authzSucc, APR_HOOK_MIDDLE); +#endif +} + +/* + * set of configuration primitives + */ +const command_rec oidc_config_cmds[] = { + + AP_INIT_TAKE1("OIDCProviderIssuer", oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, provider.issuer), + RSRC_CONF, "OpenID Connect OP issuer identifier."), + AP_INIT_TAKE1("OIDCProviderAuthorizationEndpoint", + oidc_set_https_slot, + (void *)APR_OFFSETOF(oidc_cfg, provider.authorization_endpoint_url), + RSRC_CONF, + "Define the OpenID OP Authorization Endpoint URL (e.g.: https://localhost:9031/as/authorization.oauth2)"), + AP_INIT_TAKE1("OIDCProviderTokenEndpoint", + oidc_set_https_slot, + (void *)APR_OFFSETOF(oidc_cfg, provider.token_endpoint_url), + RSRC_CONF, + "Define the OpenID OP Token Endpoint URL (e.g.: https://localhost:9031/as/token.oauth2)"), + AP_INIT_TAKE1("OIDCProviderTokenEndpointAuth", + oidc_set_endpoint_auth_slot, + (void *)APR_OFFSETOF(oidc_cfg, provider.token_endpoint_auth), + RSRC_CONF, + "Specify an authentication method for the OpenID OP Token Endpoint (e.g.: client_secret_basic)"), + AP_INIT_TAKE1("OIDCProviderUserInfoEndpoint", + oidc_set_https_slot, + (void *)APR_OFFSETOF(oidc_cfg, provider.userinfo_endpoint_url), + RSRC_CONF, + "Define the OpenID OP UserInfo Endpoint URL (e.g.: https://localhost:9031/idp/userinfo.openid)"), + AP_INIT_TAKE1("OIDCProviderJwksUri", + oidc_set_https_slot, + (void *)APR_OFFSETOF(oidc_cfg, provider.jwks_uri), + RSRC_CONF, + "Define the OpenID OP JWKS URL (e.g.: https://macbook:9031/pf/JWKS)"), + AP_INIT_TAKE1("OIDCResponseType", + oidc_set_response_type, + (void *)APR_OFFSETOF(oidc_cfg, provider.response_type), + RSRC_CONF, + "The response type (or OpenID Connect Flow) used; must be one of \"code\", \"id_token\", \"id_token token\" or \"token id_token\" (serves as default value for discovered OPs too)"), + AP_INIT_TAKE1("OIDCIDTokenAlg", oidc_set_id_token_alg, + (void *)APR_OFFSETOF(oidc_cfg, id_token_alg), + RSRC_CONF, + "The algorithm that the OP should use to sign the id_token (used only in dynamic client registration); must be one of [RS256|RS384|RS512|PS256|PS384|PS512]"), + AP_INIT_FLAG("OIDCSSLValidateServer", + oidc_set_flag_slot, + (void*)APR_OFFSETOF(oidc_cfg, provider.ssl_validate_server), + RSRC_CONF, + "Require validation of the OpenID Connect OP SSL server certificate for successful authentication (On or Off)"), + AP_INIT_TAKE1("OIDCClientName", oidc_set_string_slot, + (void *) APR_OFFSETOF(oidc_cfg, provider.client_name), + RSRC_CONF, + "Define the (client_name) name that the client uses for dynamic registration to the OP."), + AP_INIT_TAKE1("OIDCClientContact", oidc_set_string_slot, + (void *) APR_OFFSETOF(oidc_cfg, provider.client_contact), + RSRC_CONF, + "Define the contact that the client registers in dynamic registration with the OP."), + AP_INIT_TAKE1("OIDCScope", oidc_set_string_slot, + (void *) APR_OFFSETOF(oidc_cfg, provider.scope), + RSRC_CONF, + "Define the OpenID Connect scope that is requested from the OP."), + AP_INIT_TAKE1("OIDCJWKSRefreshInterval", + oidc_set_int_slot, + (void*)APR_OFFSETOF(oidc_cfg, provider.jwks_refresh_interval), + RSRC_CONF, + "Duration in seconds after which retrieved JWS should be refreshed."), + AP_INIT_TAKE1("OIDCIDTokenIatSlack", + oidc_set_int_slot, + (void*)APR_OFFSETOF(oidc_cfg, provider.idtoken_iat_slack), + RSRC_CONF, + "Acceptable offset (both before and after) for checking the \"iat\" (= issued at) timestamp in the id_token."), + + AP_INIT_TAKE1("OIDCClientID", oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, provider.client_id), + RSRC_CONF, + "Client identifier used in calls to OpenID Connect OP."), + AP_INIT_TAKE1("OIDCClientSecret", oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, provider.client_secret), + RSRC_CONF, + "Client secret used in calls to OpenID Connect OP."), + + AP_INIT_TAKE1("OIDCRedirectURI", oidc_set_url_slot, + (void *)APR_OFFSETOF(oidc_cfg, redirect_uri), + RSRC_CONF, + "Define the Redirect URI (e.g.: https://localhost:9031/protected/example/)"), + AP_INIT_TAKE1("OIDCDiscoverURL", oidc_set_url_slot, + (void *)APR_OFFSETOF(oidc_cfg, discover_url), + RSRC_CONF, + "Defines an external IDP Discovery page"), + AP_INIT_TAKE1("OIDCCookieDomain", + oidc_set_cookie_domain, NULL, RSRC_CONF, + "Specify domain element for OIDC session cookie."), + AP_INIT_TAKE1("OIDCCryptoPassphrase", + oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, crypto_passphrase), + RSRC_CONF, + "Passphrase used for AES crypto on cookies and state."), + AP_INIT_TAKE1("OIDCClaimDelimiter", + oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, claim_delimiter), + RSRC_CONF, + "The delimiter to use when setting multi-valued claims in the HTTP headers."), + AP_INIT_TAKE1("OIDCClaimPrefix", oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, claim_prefix), + RSRC_CONF, + "The prefix to use when setting claims in the HTTP headers."), + + AP_INIT_TAKE1("OIDCOAuthClientID", oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, oauth.client_id), + RSRC_CONF, + "Client identifier used in calls to OAuth 2.0 Authorization server validation calls."), + AP_INIT_TAKE1("OIDCOAuthClientSecret", + oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, oauth.client_secret), + RSRC_CONF, + "Client secret used in calls to OAuth 2.0 Authorization server validation calls."), + AP_INIT_TAKE1("OIDCOAuthEndpoint", oidc_set_https_slot, + (void *)APR_OFFSETOF(oidc_cfg, oauth.validate_endpoint_url), + RSRC_CONF, + "Define the OAuth AS Validation Endpoint URL (e.g.: https://localhost:9031/as/token.oauth2)"), + AP_INIT_TAKE1("OIDCOAuthEndpointAuth", + oidc_set_endpoint_auth_slot, + (void *)APR_OFFSETOF(oidc_cfg, oauth.validate_endpoint_auth), + RSRC_CONF, + "Specify an authentication method for the OAuth AS Validation Endpoint (e.g.: client_auth_basic)"), + AP_INIT_FLAG("OIDCOAuthSSLValidateServer", + oidc_set_flag_slot, + (void*)APR_OFFSETOF(oidc_cfg, oauth.ssl_validate_server), + RSRC_CONF, + "Require validation of the OAuth 2.0 AS Validation Endpoint SSL server certificate for successful authentication (On or Off)"), + + AP_INIT_TAKE1("OIDCHTTPTimeoutLong", oidc_set_int_slot, + (void*)APR_OFFSETOF(oidc_cfg, http_timeout_long), + RSRC_CONF, + "Timeout for long duration HTTP calls (default)."), + AP_INIT_TAKE1("OIDCHTTPTimeoutShort", oidc_set_int_slot, + (void*)APR_OFFSETOF(oidc_cfg, http_timeout_short), + RSRC_CONF, + "Timeout for short duration HTTP calls (registry/discovery)."), + AP_INIT_TAKE1("OIDCStateTimeout", oidc_set_int_slot, + (void*)APR_OFFSETOF(oidc_cfg, state_timeout), + RSRC_CONF, + "Time to live in seconds for state parameter (cq. interval in which the authorization request and the corresponding response need to be completed)."), + + AP_INIT_TAKE1("OIDCMetadataDir", oidc_set_dir_slot, + (void*)APR_OFFSETOF(oidc_cfg, metadata_dir), + RSRC_CONF, + "Directory that contains provider and client metadata files."), + AP_INIT_TAKE1("OIDCSessionType", oidc_set_session_type, + (void*)APR_OFFSETOF(oidc_cfg, session_type), + RSRC_CONF, + "OpenID Connect session storage type (Apache 2.0/2.2 only). Must be one of \"file\" or \"cookie\"."), + AP_INIT_FLAG("OIDCScrubRequestHeaders", + oidc_set_flag_slot, + (void *) APR_OFFSETOF(oidc_cfg, scrub_request_headers), + RSRC_CONF, + "Scrub user name and claim headers from the user's request."), + + AP_INIT_TAKE1("OIDCAuthNHeader", ap_set_string_slot, + (void *) APR_OFFSETOF(oidc_dir_cfg, authn_header), + ACCESS_CONF|OR_AUTHCFG, + "Specify the HTTP header variable to set with the name of the authenticated user. By default no headers are added."), + AP_INIT_TAKE1("OIDCCookiePath", ap_set_string_slot, + (void *) APR_OFFSETOF(oidc_dir_cfg, cookie_path), + ACCESS_CONF|OR_AUTHCFG, + "Define the cookie path for the session cookie."), + AP_INIT_TAKE1("OIDCCookie", ap_set_string_slot, + (void *) APR_OFFSETOF(oidc_dir_cfg, cookie), + ACCESS_CONF|OR_AUTHCFG, + "Define the cookie name for the session cookie."), + + AP_INIT_TAKE1("OIDCCacheType", oidc_set_cache_type, + (void*)APR_OFFSETOF(oidc_cfg, cache), RSRC_CONF, + "Cache type; must be one of \"file\", \"memcache\" or \"shm\"."), + + AP_INIT_TAKE1("OIDCCacheDir", oidc_set_dir_slot, + (void*)APR_OFFSETOF(oidc_cfg, cache_file_dir), + RSRC_CONF, + "Directory used for file-based caching."), + AP_INIT_TAKE1("OIDCCacheFileCleanInterval", oidc_set_int_slot, + (void*)APR_OFFSETOF(oidc_cfg, cache_file_clean_interval), + RSRC_CONF, + "Cache file clean interval in seconds."), + AP_INIT_TAKE1("OIDCMemCacheServers", + oidc_set_string_slot, + (void*)APR_OFFSETOF(oidc_cfg, cache_memcache_servers), + RSRC_CONF, + "Memcache servers used for caching (space separated list of [:] tuples)"), + AP_INIT_TAKE1("OIDCCacheShmMax", oidc_set_int_slot, + (void*)APR_OFFSETOF(oidc_cfg, cache_shm_size_max), + RSRC_CONF, + "Maximum number of cache entries to use for \"shm\" caching."), + + { NULL } +}; diff --git a/src/crypto.c b/src/crypto.c new file mode 100644 index 00000000..340c163d --- /dev/null +++ b/src/crypto.c @@ -0,0 +1,366 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * based on http://saju.net.in/code/misc/openssl_aes.c.txt + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "mod_auth_openidc.h" + +/* + * initialize the crypto context in the server configuration record; the passphrase is set already + */ +static apr_byte_t oidc_crypto_init(oidc_cfg *cfg, server_rec *s) { + + if (cfg->encrypt_ctx != NULL) + return TRUE; + + unsigned char *key_data = (unsigned char *) cfg->crypto_passphrase; + int key_data_len = strlen(cfg->crypto_passphrase); + + unsigned int s_salt[] = { 41892, 72930 }; + unsigned char *salt = (unsigned char *) &s_salt; + + int i, nrounds = 5; + unsigned char key[32], iv[32]; + + /* + * Gen key & IV for AES 256 CBC mode. A SHA1 digest is used to hash the supplied key material. + * nrounds is the number of times the we hash the material. More rounds are more secure but + * slower. + */ + i = EVP_BytesToKey(EVP_aes_256_cbc(), EVP_sha1(), salt, key_data, + key_data_len, nrounds, key, iv); + if (i != 32) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, + "oidc_crypto_init: key size must be 256 bits!"); + return FALSE; + } + + cfg->encrypt_ctx = apr_palloc(s->process->pool, sizeof(EVP_CIPHER_CTX)); + cfg->decrypt_ctx = apr_palloc(s->process->pool, sizeof(EVP_CIPHER_CTX)); + + /* initialize the encoding context */ + EVP_CIPHER_CTX_init(cfg->encrypt_ctx); + if (!EVP_EncryptInit_ex(cfg->encrypt_ctx, EVP_aes_256_cbc(), NULL, key, + iv)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, + "oidc_crypto_init: EVP_EncryptInit_ex on the encrypt context failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return FALSE; + } + + /* initialize the decoding context */ + EVP_CIPHER_CTX_init(cfg->decrypt_ctx); + if (!EVP_DecryptInit_ex(cfg->decrypt_ctx, EVP_aes_256_cbc(), NULL, key, + iv)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, + "oidc_crypto_init: EVP_DecryptInit_ex on the decrypt context failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return FALSE; + } + + return TRUE; +} + +/* + * AES encrypt plaintext + */ +unsigned char *oidc_crypto_aes_encrypt(request_rec *r, oidc_cfg *cfg, + unsigned char *plaintext, int *len) { + + if (oidc_crypto_init(cfg, r->server) == FALSE) return NULL; + + /* max ciphertext len for a n bytes of plaintext is n + AES_BLOCK_SIZE -1 bytes */ + int c_len = *len + AES_BLOCK_SIZE, f_len = 0; + unsigned char *ciphertext = apr_palloc(r->pool, c_len); + + /* allows reusing of 'e' for multiple encryption cycles */ + if (!EVP_EncryptInit_ex(cfg->encrypt_ctx, NULL, NULL, NULL, NULL)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_aes_encrypt: EVP_EncryptInit_ex failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + + /* update ciphertext, c_len is filled with the length of ciphertext generated, len is the size of plaintext in bytes */ + if (!EVP_EncryptUpdate(cfg->encrypt_ctx, ciphertext, &c_len, plaintext, + *len)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_aes_encrypt: EVP_EncryptUpdate failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + + /* update ciphertext with the final remaining bytes */ + if (!EVP_EncryptFinal_ex(cfg->encrypt_ctx, ciphertext + c_len, &f_len)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_aes_encrypt: EVP_EncryptFinal_ex failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + + *len = c_len + f_len; + + return ciphertext; +} + +/* + * AES decrypt ciphertext + */ +unsigned char *oidc_crypto_aes_decrypt(request_rec *r, oidc_cfg *cfg, + unsigned char *ciphertext, int *len) { + + if (oidc_crypto_init(cfg, r->server) == FALSE) return NULL; + + /* because we have padding ON, we must allocate an extra cipher block size of memory */ + int p_len = *len, f_len = 0; + unsigned char *plaintext = apr_palloc(r->pool, p_len + AES_BLOCK_SIZE); + + /* allows reusing of 'e' for multiple encryption cycles */ + if (!EVP_DecryptInit_ex(cfg->decrypt_ctx, NULL, NULL, NULL, NULL)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_aes_decrypt: EVP_DecryptInit_ex failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + + /* update plaintext, p_len is filled with the length of plaintext generated, len is the size of ciphertext in bytes */ + if (!EVP_DecryptUpdate(cfg->decrypt_ctx, plaintext, &p_len, ciphertext, + *len)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_aes_decrypt: EVP_DecryptUpdate failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + + /* update plaintext with the final remaining bytes */ + if (!EVP_DecryptFinal_ex(cfg->decrypt_ctx, plaintext + p_len, &f_len)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_aes_decrypt: EVP_DecryptFinal_ex failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + + *len = p_len + f_len; + + return plaintext; +} + +/* + * return OpenSSL digest for JWK algorithm + */ +char *oidc_crypto_jwt_alg2digest(const char *alg) { + if ((strcmp(alg, "RS256") == 0) || (strcmp(alg, "PS256") == 0) || (strcmp(alg, "HS256") == 0)) { + return "sha256"; + } + if ((strcmp(alg, "RS384") == 0) || (strcmp(alg, "PS384") == 0) || (strcmp(alg, "HS384") == 0)) { + return "sha384"; + } + if ((strcmp(alg, "RS512") == 0) || (strcmp(alg, "PS512") == 0) || (strcmp(alg, "HS512") == 0)) { + return "sha512"; + } + if (strcmp(alg, "NONE") == 0) { + return "NONE"; + } + return NULL; +} + +/* + * return OpenSSL padding type for JWK RSA algorithm + */ +static int oidc_crypto_jwt_alg2rsa_padding(const char *alg) { + if ((strcmp(alg, "RS256") == 0) || (strcmp(alg, "RS384") == 0) || (strcmp(alg, "RS512") == 0)) { + return RSA_PKCS1_PADDING; + } + if ((strcmp(alg, "PS256") == 0) || (strcmp(alg, "PS384") == 0) || (strcmp(alg, "PS512") == 0)) { + return RSA_PKCS1_PSS_PADDING; + } + return -1; +} + +static const EVP_MD *oidc_crypto_alg2evp(request_rec *r, const char *alg) { + const EVP_MD *result = NULL; + + char *digest = oidc_crypto_jwt_alg2digest(alg); + + if (digest == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_alg2evp: unsupported algorithm: %s", alg); + return NULL; + } + + result = EVP_get_digestbyname(digest); + + if (result == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_alg2evp: EVP_get_digestbyname failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + + return result; +} + +/* + * verify RSA signature + */ +apr_byte_t oidc_crypto_rsa_verify(request_rec *r, const char *alg, unsigned char* sig, int sig_len, unsigned char* msg, + int msg_len, unsigned char *mod, int mod_len, unsigned char *exp, int exp_len) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_crypto_rsa_verify: entering (%s)", alg); + + const EVP_MD *digest = NULL; + if ((digest = oidc_crypto_alg2evp(r, alg)) == NULL) return FALSE; + + apr_byte_t rc = FALSE; + + EVP_MD_CTX ctx; + EVP_MD_CTX_init(&ctx); + + RSA * pubkey = RSA_new(); + + BIGNUM * modulus = BN_new(); + BIGNUM * exponent = BN_new(); + + BN_bin2bn(mod, mod_len, modulus); + BN_bin2bn(exp, exp_len, exponent); + + pubkey->n = modulus; + pubkey->e = exponent; + + EVP_PKEY* pRsaKey = EVP_PKEY_new(); + if (!EVP_PKEY_assign_RSA(pRsaKey, pubkey)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_rsa_verify: EVP_PKEY_assign_RSA failed: %s", ERR_error_string(ERR_get_error(), NULL)); + pRsaKey = NULL; + goto end; + } + + ctx.pctx = EVP_PKEY_CTX_new(pRsaKey, NULL); + if (!EVP_PKEY_verify_init(ctx.pctx)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_rsa_verify: EVP_PKEY_verify_init failed: %s", ERR_error_string(ERR_get_error(), NULL)); + goto end; + } + if (!EVP_PKEY_CTX_set_rsa_padding(ctx.pctx, oidc_crypto_jwt_alg2rsa_padding(alg))) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_rsa_verify: EVP_PKEY_CTX_set_rsa_padding failed: %s", ERR_error_string(ERR_get_error(), NULL)); + goto end; + } + + if (!EVP_VerifyInit_ex(&ctx, digest, NULL)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_rsa_verify: EVP_VerifyInit_ex failed: %s", ERR_error_string(ERR_get_error(), NULL)); + goto end; + } + + if (!EVP_VerifyUpdate(&ctx, msg, msg_len)){ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_rsa_verify: EVP_VerifyUpdate failed: %s", ERR_error_string(ERR_get_error(), NULL)); + goto end; + } + + if (!EVP_VerifyFinal(&ctx, sig, sig_len, pRsaKey)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_rsa_verify: EVP_VerifyFinal failed: %s", ERR_error_string(ERR_get_error(), NULL)); + goto end; + } + + rc = TRUE; + +end: + if (pRsaKey) { + EVP_PKEY_free(pRsaKey); + } else if (pubkey) { + RSA_free(pubkey); + } + EVP_MD_CTX_cleanup(&ctx); + + return rc; +} + +/* + * verify HOIDC signature + */ +apr_byte_t oidc_crypto_hoidc_verify(request_rec *r, const char *alg, unsigned char* sig, int sig_len, unsigned char* msg, + int msg_len, unsigned char *key, int key_len) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_crypto_hoidc_verify: entering (%s)", alg); + + const EVP_MD *digest = NULL; + if ((digest = oidc_crypto_alg2evp(r, alg)) == NULL) return FALSE; + + unsigned int md_len = 0; + unsigned char md[EVP_MAX_MD_SIZE]; + + if (!HMAC(digest, key, key_len, msg, msg_len, md, &md_len)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_hoidc_verify: HOIDC failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return FALSE; + } + + if (md_len != sig_len) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_hoidc_verify: hash length does not match signature length"); + return FALSE; + } + + if (memcmp(md, sig, md_len) != 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_crypto_hoidc_verify: HOIDC verification failed"); + return FALSE; + } + + return TRUE; +} diff --git a/src/metadata.c b/src/metadata.c new file mode 100644 index 00000000..08804eb0 --- /dev/null +++ b/src/metadata.c @@ -0,0 +1,999 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * OpenID Connect metadata handling routines, for both OP discovery and client registration + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include +#include +#include + +#include +#include + +// for converting JWKs +#include +#include +#include +#include + +#include "mod_auth_openidc.h" + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +#define OIDC_METADATA_SUFFIX_PROVIDER "provider" +#define OIDC_METADATA_SUFFIX_CLIENT "client" +#define OIDC_METADATA_SUFFIX_JWKS "jwks" + +/* + * get the metadata filename for a specified issuer (cq. urlencode it) + */ +static const char *oidc_metadata_issuer_to_filename(request_rec *r, + const char *issuer) { + + /* strip leading https:// */ + char *p = strstr(issuer, "https://"); + if (p == issuer) { + p = apr_pstrdup(r->pool, issuer + strlen("https://")); + } else { + p = apr_pstrdup(r->pool, issuer); + } + + /* strip trailing '/' */ + int n = strlen(p); + if (p[n - 1] == '/') p[n - 1] = '\0'; + + return oidc_util_escape_string(r, p); +} + +/* + * get the issuer from a metadata filename (cq. urldecode it) + */ +static const char *oidc_metadata_filename_to_issuer(request_rec *r, + const char *filename) { + char *result = apr_pstrdup(r->pool, filename); + char *p = strrchr(result, '.'); + *p = '\0'; + p = oidc_util_unescape_string(r, result); + return (strcmp(p, "accounts.google.com") == 0) ? p : apr_psprintf(r->pool, "https://%s", p); +} + +/* + * get the full path to the metadata file for a specified issuer and directory + */ +static const char *oidc_metadata_file_path(request_rec *r, oidc_cfg *cfg, + const char *issuer, const char *type) { + return apr_psprintf(r->pool, "%s/%s.%s", cfg->metadata_dir, + oidc_metadata_issuer_to_filename(r, issuer), type); +} + +/* + * get the full path to the provider metadata file for a specified issuer + */ +static const char *oidc_metadata_provider_file_path(request_rec *r, + const char *issuer) { + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + return oidc_metadata_file_path(r, cfg, issuer, + OIDC_METADATA_SUFFIX_PROVIDER); +} + +/* + * get the full path to the client metadata file for a specified issuer + */ +static const char *oidc_metadata_client_file_path(request_rec *r, + const char *issuer) { + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + return oidc_metadata_file_path(r, cfg, issuer, OIDC_METADATA_SUFFIX_CLIENT); +} + +/* + * get the full path to the jwks metadata file for a specified issuer + */ +static const char *oidc_metadata_jwks_cache_key(request_rec *r, + const char *issuer) { + return apr_psprintf(r->pool, "%s.jwks", issuer); +} + +/* + * read a JSON metadata file from disk + */ +static apr_byte_t oidc_metadata_file_read_json(request_rec *r, const char *path, + apr_json_value_t **result) { + apr_status_t rc = APR_SUCCESS; + char *buf = NULL; + + /* read the file contents */ + if (oidc_util_file_read(r, path, &buf) == FALSE) + return FALSE; + + /* decode the JSON contents of the buffer */ + if ((rc = apr_json_decode(result, buf, strlen(buf), r->pool)) != APR_SUCCESS) { + /* something went wrong */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_file_read_json: JSON parsing (%s) returned an error: (%d)", + path, rc); + return FALSE; + } + + if ((*result == NULL) || ((*result)->type != APR_JSON_OBJECT)) { + /* oops, no JSON */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_file_read_json: parsed JSON from (%s) did not contain a JSON object", + path); + return FALSE; + } + + /* log successful metadata retrieval */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_file_read_json: JSON parsed from file \"%s\"", path); + + return TRUE; +} + +/* + * check to see if JSON provider metadata is valid + */ +static apr_byte_t oidc_metadata_provider_is_valid(request_rec *r, + apr_json_value_t *j_provider, const char *issuer) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_provider_is_valid: entering"); + + /* get the "issuer" from the provider metadata and double-check that it matches what we looked for */ + apr_json_value_t *j_issuer = apr_hash_get(j_provider->value.object, + "issuer", APR_HASH_KEY_STRING); + if ((j_issuer == NULL) || (j_issuer->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_provider_is_valid: provider JSON object did not contain an \"issuer\" string"); + return FALSE; + } + + /* check that the issuer matches */ + if (oidc_util_issuer_match(issuer, j_issuer->value.string.p) == FALSE) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_provider_is_valid: requested issuer (%s) does not match the \"issuer\" value in the provider metadata file: %s", + issuer, j_issuer->value.string.p); + return FALSE; + } + + /* verify that the provider supports the a flow that we implement */ + apr_json_value_t *j_response_types_supported = apr_hash_get( + j_provider->value.object, "response_types_supported", + APR_HASH_KEY_STRING); + if ((j_response_types_supported != NULL) + && (j_response_types_supported->type == APR_JSON_ARRAY)) { + if ((oidc_util_json_array_has_value(r, j_response_types_supported, + "code") == FALSE) + && (oidc_util_json_array_has_value(r, + j_response_types_supported, "id_token") == FALSE) + && (oidc_util_json_array_has_value(r, + j_response_types_supported, "token id_token") == FALSE) + && (oidc_util_json_array_has_value(r, + j_response_types_supported, "id_token token") == FALSE)) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_provider_is_valid: could not find a supported value [\"code\" | \"id_token\" | \"token id_token\" | \"id_token token\"] in provider metadata for entry \"response_types_supported\"; assuming that \"code\" flow is supported..."); + //return FALSE; + } + } else { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_provider_is_valid: provider JSON object did not contain a \"response_types_supported\" array; assuming that \"code\" flow is supported..."); + // TODO: hey, this is required-by-spec stuff right? + } + + /* get a handle to the authorization endpoint */ + apr_json_value_t *j_authorization_endpoint = apr_hash_get( + j_provider->value.object, "authorization_endpoint", + APR_HASH_KEY_STRING); + if ((j_authorization_endpoint == NULL) + || (j_authorization_endpoint->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_provider_is_valid: provider JSON object did not contain an \"authorization_endpoint\" string"); + return FALSE; + } + + /* get a handle to the token endpoint */ + apr_json_value_t *j_token_endpoint = apr_hash_get(j_provider->value.object, + "token_endpoint", APR_HASH_KEY_STRING); + if ((j_token_endpoint == NULL) + || (j_token_endpoint->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_provider_is_valid: provider JSON object did not contain a \"token_endpoint\" string"); + //return FALSE; + } + + /* get a handle to the user_info endpoint */ + apr_json_value_t *j_userinfo_endpoint = apr_hash_get( + j_provider->value.object, "userinfo_endpoint", APR_HASH_KEY_STRING); + if ((j_userinfo_endpoint != NULL) + && (j_userinfo_endpoint->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_provider_is_valid: provider JSON object contains a \"userinfo_endpoint\" entry, but it is not a string value"); + } + // TODO: check for valid URL + + /* get a handle to the jwks_uri */ + apr_json_value_t *j_jwks_uri = apr_hash_get(j_provider->value.object, + "jwks_uri", APR_HASH_KEY_STRING); + if ((j_jwks_uri == NULL) || (j_jwks_uri->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_provider_is_valid: provider JSON object did not contain a \"jwks_uri\" string"); + //return FALSE; + } + + /* find out what type of authentication the token endpoint supports (we only support post or basic) */ + apr_json_value_t *j_token_endpoint_auth_methods_supported = apr_hash_get( + j_provider->value.object, "token_endpoint_auth_methods_supported", + APR_HASH_KEY_STRING); + if ((j_token_endpoint_auth_methods_supported == NULL) + || (j_token_endpoint_auth_methods_supported->type != APR_JSON_ARRAY)) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_provider_is_valid: provider JSON object did not contain a \"token_endpoint_auth_methods_supported\" array, assuming \"client_secret_basic\" is supported"); + } else { + int i; + for (i = 0; + i < j_token_endpoint_auth_methods_supported->value.array->nelts; + i++) { + apr_json_value_t *elem = APR_ARRAY_IDX( + j_token_endpoint_auth_methods_supported->value.array, i, + apr_json_value_t *); + if (elem->type != APR_JSON_STRING) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_provider_is_valid: unhandled in-array JSON object type [%d] in provider metadata for entry \"token_endpoint_auth_methods_supported\"", + elem->type); + continue; + } + if (strcmp(elem->value.string.p, "client_secret_post") == 0) { + break; + } + if (strcmp(elem->value.string.p, "client_secret_basic") == 0) { + break; + } + } + if (i == j_token_endpoint_auth_methods_supported->value.array->nelts) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_provider_is_valid: could not find a supported value [client_secret_post|client_secret_basic] in provider metadata for entry \"token_endpoint_auth_methods_supported\""); + return FALSE; + } + } + + return TRUE; +} + +/* + * check to see if dynamically registered JSON client metadata has not expired + */ +static apr_byte_t oidc_metadata_client_is_valid(request_rec *r, + apr_json_value_t *j_client, const char *issuer) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_client_is_valid: entering"); + + /* get a handle to the client_id we need to use for this provider */ + apr_json_value_t *j_client_id = apr_hash_get(j_client->value.object, + "client_id", APR_HASH_KEY_STRING); + if ((j_client_id == NULL) || (j_client_id->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_client_is_valid: client JSON object did not contain a \"client_id\" string"); + return FALSE; + } + + /* get a handle to the client_secret we need to use for this provider */ + apr_json_value_t *j_client_secret = apr_hash_get(j_client->value.object, + "client_secret", APR_HASH_KEY_STRING); + if ((j_client_secret == NULL) + || (j_client_secret->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_client_is_valid: client JSON object did not contain a \"client_secret\" string"); + return FALSE; + } + + /* the expiry timestamp from the JSON object */ + apr_json_value_t *expires_at = apr_hash_get(j_client->value.object, + "client_secret_expires_at", APR_HASH_KEY_STRING); + if ((expires_at == NULL) || (expires_at->type != APR_JSON_LONG)) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_client_is_valid: client metadata for \"%s\" did not contain a \"client_secret_expires_at\" setting", + issuer); + /* assume that it never expires */ + return TRUE; + } + + /* see if it is unrestricted */ + if (expires_at->value.lnumber == 0) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_client_is_valid: client metadata for \"%s\" never expires (client_secret_expires_at=0)", + issuer); + return TRUE; + } + + /* check if the value >= now */ + if (apr_time_sec(apr_time_now()) > expires_at->value.lnumber) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_client_is_valid: client secret for \"%s\" expired", + issuer); + return FALSE; + } + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_client_is_valid: client secret for \"%s\" has not expired, return OK", + issuer); + + /* all ok, not expired */ + return TRUE; +} + +/* + * checks if a parsed JWKs file is a valid one, cq. contains "keys" + */ +static apr_byte_t oidc_metadata_jwks_is_valid(request_rec *r, + apr_json_value_t *j_jwks, const char *issuer) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_jwks_is_valid: entering"); + + apr_json_value_t *keys = apr_hash_get(j_jwks->value.object, "keys", + APR_HASH_KEY_STRING); + if ((keys == NULL) || (keys->type != APR_JSON_ARRAY)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_jwks_is_valid: JWKS JSON object did not contain a \"keys\" array"); + return FALSE; + } + return TRUE; +} + +/* + * write JSON metadata to a file + */ +static apr_byte_t oidc_metadata_file_write(request_rec *r, const char *path, + const char *data) { + + // TODO: completely erase the contents of the file if it already exists.... + + apr_file_t *fd = NULL; + apr_status_t rc = APR_SUCCESS; + apr_size_t bytes_written = 0; + char s_err[128]; + + /* try to open the metadata file for writing, creating it if it does not exist */ + if ((rc = apr_file_open(&fd, path, (APR_FOPEN_WRITE | APR_FOPEN_CREATE), + APR_OS_DEFAULT, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_file_write: file \"%s\" could not be opened (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + return FALSE; + } + + /* lock the file and move the write pointer to the start of it */ + apr_file_lock(fd, APR_FLOCK_EXCLUSIVE); + apr_off_t begin = 0; + apr_file_seek(fd, APR_SET, &begin); + + /* calculate the length of the data, which is a string length */ + apr_size_t len = strlen(data); + + /* (blocking) write the number of bytes in the buffer */ + rc = apr_file_write_full(fd, data, len, &bytes_written); + + /* check for a system error */ + if (rc != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_file_write: could not write to: \"%s\" (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + return FALSE; + } + + /* check that all bytes from the header were written */ + if (bytes_written != len) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_file_write: could not write enough bytes to: \"%s\", bytes_written (%" APR_SIZE_T_FMT ") != len (%" APR_SIZE_T_FMT ")", + path, bytes_written, len); + return FALSE; + } + + /* unlock and close the written file */ + apr_file_unlock(fd); + apr_file_close(fd); + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_file_write: file \"%s\" written; number of bytes (%" APR_SIZE_T_FMT ")", + path, len); + + return TRUE; +} + +/* callback function type for checking metadata validity (provider or client) */ +typedef apr_byte_t (*oidc_is_valid_function_t)(request_rec *, + apr_json_value_t *, const char *); + +/* + * helper function to get the JSON (client or provider) metadata from the specified file path and check its validity + */ +static apr_byte_t oidc_metadata_get_and_check(request_rec *r, const char *path, + const char *issuer, oidc_is_valid_function_t metadata_is_valid, + apr_json_value_t **j_metadata) { + + apr_finfo_t fi; + apr_status_t rc = APR_SUCCESS; + char s_err[128]; + + /* read the metadata from a file in to a variable */ + if (oidc_metadata_file_read_json(r, path, j_metadata) == FALSE) + goto error_delete; + + /* we've got metadata that is JSON and no error-JSON, but now we check provider/client validity */ + if (metadata_is_valid(r, *j_metadata, issuer) == FALSE) + goto error_delete; + + /* all OK if we got here */ + return TRUE; + +error_delete: + + /* + * this is expired or otherwise invalid metadata, we're probably going to get + * new metadata, so delete the file first, if it (still) exists at all + */ + if (apr_stat(&fi, path, APR_FINFO_MTIME, r->pool) == APR_SUCCESS) { + + if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_get_and_check: could not delete invalid metadata file %s (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + } else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_get_and_check: removed invalid metadata file %s", + path); + } + } + + return FALSE; +} + +/* + * helper function to retrieve (client or provider) metadata from a URL, check it and store it + */ +static apr_byte_t oidc_metadata_retrieve_and_store(request_rec *r, + oidc_cfg *cfg, const char *url, int action, apr_table_t *params, + int ssl_validate_server, const char *issuer, + oidc_is_valid_function_t f_is_valid, const char *path, + apr_json_value_t **j_metadata) { + const char *response = NULL; + + /* no valid provider metadata, get it at the specified URL with the specified parameters */ + if (oidc_util_http_call(r, url, action, params, NULL, NULL, + ssl_validate_server, &response, cfg->http_timeout_short) == FALSE) + return FALSE; + + /* decode and see if it is not an error response somehow */ + if (oidc_util_decode_json_and_check_error(r, response, j_metadata) == FALSE) + return FALSE; + + /* check to see if it is valid metadata */ + if (f_is_valid(r, *j_metadata, issuer) == FALSE) + return FALSE; + + /* since it is valid, write the obtained provider metadata file */ + if (oidc_metadata_file_write(r, path, response) == FALSE) + return FALSE; + + /* all OK */ + return TRUE; +} + +/* + * helper function to get the JWKs for the specified issuer + */ +static apr_byte_t oidc_metadata_jwks_retrieve_and_store(request_rec *r, + oidc_cfg *cfg, oidc_provider_t *provider, apr_json_value_t **j_jwks) { + + const char *response = NULL; + + /* no valid provider metadata, get it at the specified URL with the specified parameters */ + if (oidc_util_http_call(r, provider->jwks_uri, OIDC_HTTP_GET, NULL, NULL, + NULL, provider->ssl_validate_server, &response, + cfg->http_timeout_short) == FALSE) + return FALSE; + + /* decode and see if it is not an error response somehow */ + if (oidc_util_decode_json_and_check_error(r, response, j_jwks) == FALSE) + return FALSE; + + /* check to see if it is valid metadata */ + if (oidc_metadata_jwks_is_valid(r, *j_jwks, provider->issuer) == FALSE) + return FALSE; + + /* store the JWKs in the cache */ + cfg->cache->set(r, oidc_metadata_jwks_cache_key(r, provider->issuer), + response, + apr_time_now() + apr_time_from_sec(provider->jwks_refresh_interval)); + + return TRUE; +} + +/* + * return JWKs for the specified issuer + */ +apr_byte_t oidc_metadata_jwks_get(request_rec *r, oidc_cfg *cfg, + oidc_provider_t *provider, apr_json_value_t **j_jwks, + apr_byte_t *refresh) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_jwks_get: entering (issuer=%s, refresh=%d)", + provider->issuer, *refresh); + + /* see if we need to do a forced refresh */ + if (*refresh == TRUE) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_jwks_get: doing a forced refresh of the JWKs for issuer \"%s\"", + provider->issuer); + if (oidc_metadata_jwks_retrieve_and_store(r, cfg, provider, + j_jwks) == TRUE) + return TRUE; + // else: fallback on any cached JWKs + } + + /* see if the JWKs is cached */ + const char *value = NULL; + cfg->cache->get(r, oidc_metadata_jwks_cache_key(r, provider->issuer), + &value); + + if (value == NULL) { + /* it is non-existing or expired: do a forced refresh */ + *refresh = TRUE; + return oidc_metadata_jwks_retrieve_and_store(r, cfg, provider, j_jwks); + } + + /* decode and see if it is not an error response somehow */ + if (oidc_util_decode_json_and_check_error(r, value, j_jwks) == FALSE) + return FALSE; + + return TRUE; +} + +/* + * see if we have provider metadata and check its validity + * if not, use OpenID Connect Provider Issuer Discovery to get it, check it and store it + */ +static apr_byte_t oidc_metadata_provider_get(request_rec *r, oidc_cfg *cfg, + const char *issuer, apr_json_value_t **j_provider) { + + /* get the full file path to the provider metadata for this issuer */ + const char *provider_path = oidc_metadata_provider_file_path(r, issuer); + + /* see if we have valid metadata already, if so, return it */ + if (oidc_metadata_get_and_check(r, provider_path, issuer, + oidc_metadata_provider_is_valid, j_provider) == TRUE) + return TRUE; + + // TODO: how to do validity/expiry checks on provider metadata + + /* assemble the URL to the .well-known OpenID metadata */ + const char *url = apr_psprintf(r->pool, "%s", + ((strstr(issuer, "http://") == issuer) + || (strstr(issuer, "https://") == issuer)) ? + issuer : apr_psprintf(r->pool, "https://%s", issuer)); + url = apr_psprintf(r->pool, "%s%s.well-known/openid-configuration", url, + url[strlen(url) - 1] != '/' ? "/" : ""); + + /* try and get it from there, checking it and storing it if successful */ + return oidc_metadata_retrieve_and_store(r, cfg, url, OIDC_HTTP_GET, NULL, + cfg->provider.ssl_validate_server, issuer, + oidc_metadata_provider_is_valid, provider_path, j_provider); +} + +/* + * see if we have client metadata and check its validity + * if not, use OpenID Connect Client Registration to get it, check it and store it + */ +static apr_byte_t oidc_metadata_client_get(request_rec *r, oidc_cfg *cfg, + const char *issuer, const char *registration_url, + apr_json_value_t **j_client) { + + /* get the full file path to the provider metadata for this issuer */ + const char *client_path = oidc_metadata_client_file_path(r, issuer); + + /* see if we already have valid client metadata, if so, return TRUE */ + if (oidc_metadata_get_and_check(r, client_path, issuer, + oidc_metadata_client_is_valid, j_client) == TRUE) + return TRUE; + + /* at this point we have no valid client metadata, see if there's a registration endpoint for this provider */ + if (registration_url == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_client_get: no (valid) client metadata exists and provider JSON object did not contain a (valid) \"registration_endpoint\" string"); + return FALSE; + } + + /* go and use Dynamic Client registration to fetch ourselves new client metadata */ + apr_table_t *params = apr_table_make(r->pool, 3); + apr_table_addn(params, "client_name", cfg->provider.client_name); + + if (cfg->id_token_alg != NULL) { + apr_table_addn(params, "id_token_signed_response_alg", + cfg->id_token_alg); + } + + int action = OIDC_HTTP_POST_JSON; + + /* hack away for pre-standard PingFederate client registration... */ + if (strstr(registration_url, "idp/client-registration.openid") != NULL) { + + /* add PF specific client registration parameters */ + apr_table_addn(params, "operation", "client_register"); + apr_table_addn(params, "redirect_uris", cfg->redirect_uri); + if (cfg->provider.client_contact != NULL) { + apr_table_addn(params, "contacts", cfg->provider.client_contact); + } + + action = OIDC_HTTP_POST_FORM; + + } else { + + // TODO also hacky, we need arrays for the next two values + apr_table_addn(params, "redirect_uris", + apr_psprintf(r->pool, "[\"%s\"]", cfg->redirect_uri)); + if (cfg->provider.client_contact != NULL) { + apr_table_addn(params, "contacts", + apr_psprintf(r->pool, "[\"%s\"]", + cfg->provider.client_contact)); + } + } + + /* try and get it from there, checking it and storing it if successful */ + return oidc_metadata_retrieve_and_store(r, cfg, registration_url, action, + params, cfg->provider.ssl_validate_server, issuer, + oidc_metadata_client_is_valid, client_path, j_client); +} + +/* + * return both provider and client metadata for the specified issuer + * + * TODO: should we use a modification timestamp on client metadata to skip + * validation if it has been done recently, or is that overkill? + * + * at least it is not overkill for blacklisting providers that registration fails for + * but maybe we should just delete the provider data for those? + */ +static apr_byte_t oidc_metadata_get_provider_and_client(request_rec *r, + oidc_cfg *cfg, const char *issuer, apr_json_value_t **j_provider, + apr_json_value_t **j_client) { + + const char *registration_url = NULL; + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_metadata_get_provider_and_client: entering; issuer=\"%s\"", + issuer); + + /* see if we can get valid provider metadata (possibly bootstrapping with Discovery), if not, return FALSE */ + if (oidc_metadata_provider_get(r, cfg, issuer, j_provider) == FALSE) + return FALSE; + + /* get a reference to the registration endpoint, if it exists */ + apr_json_value_t *j_registration_endpoint = apr_hash_get( + (*j_provider)->value.object, "registration_endpoint", + APR_HASH_KEY_STRING); + if ((j_registration_endpoint != NULL) + && (j_registration_endpoint->type == APR_JSON_STRING)) { + registration_url = j_registration_endpoint->value.string.p; + } + + if (oidc_metadata_client_get(r, cfg, issuer, registration_url, + j_client) == FALSE) + return FALSE; + + /* all OK */ + return TRUE; +} + +/* + * get a list of configured OIDC providers based on the entries in the provider metadata directory + */ +apr_byte_t oidc_metadata_list(request_rec *r, oidc_cfg *cfg, + apr_array_header_t **list) { + apr_status_t rc; + apr_dir_t *dir; + apr_finfo_t fi; + char s_err[128]; + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_metadata_list: entering"); + + /* open the metadata directory */ + if ((rc = apr_dir_open(&dir, cfg->metadata_dir, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_list: error opening metadata directory '%s' (%s)", + cfg->metadata_dir, apr_strerror(rc, s_err, sizeof(s_err))); + return FALSE; + } + + /* allocate some space in the array that will hold the list of providers */ + *list = apr_array_make(r->pool, 5, sizeof(sizeof(const char*))); + /* BTW: we could estimate the number in the array based on # directory entries... */ + + /* loop over the entries in the provider metadata directory */ + while (apr_dir_read(&fi, APR_FINFO_NAME, dir) == APR_SUCCESS) { + + /* skip "." and ".." entries */ + if (fi.name[0] == '.') + continue; + /* skip other non-provider entries */ + char *ext = strrchr(fi.name, '.'); + if ((ext == NULL) + || (strcmp(++ext, OIDC_METADATA_SUFFIX_PROVIDER) != 0)) + continue; + + /* get the issuer from the filename */ + const char *issuer = oidc_metadata_filename_to_issuer(r, fi.name); + + /* pointer to the parsed JSON metadata for the provider */ + apr_json_value_t *j_provider = NULL; + /* pointer to the parsed JSON metadata for the client */ + apr_json_value_t *j_client = NULL; + + /* get the provider and client metadata, do all checks and registration if possible */ + if (oidc_metadata_get_provider_and_client(r, cfg, issuer, &j_provider, + &j_client) == FALSE) + continue; + + /* push the decoded issuer filename in to the array */ + *(const char**) apr_array_push(*list) = issuer; + } + + /* we're done, cleanup now */ + apr_dir_close(dir); + + return TRUE; +} + +/* + * find out what type of authentication we must provide to the token endpoint (we only support post or basic) + */ +static const char * oidc_metadata_token_endpoint_auth(request_rec *r, + apr_json_value_t *j_client, apr_json_value_t *j_provider) { + + const char *result = "client_secret_basic"; + + /* see if one is defined in the client metadata */ + apr_json_value_t *token_endpoint_auth_method = apr_hash_get( + j_client->value.object, "token_endpoint_auth_method", + APR_HASH_KEY_STRING); + if (token_endpoint_auth_method != NULL) { + if (token_endpoint_auth_method->type == APR_JSON_STRING) { + if (strcmp(token_endpoint_auth_method->value.string.p, + "client_secret_post") == 0) { + result = "client_secret_post"; + return result; + } + if (strcmp(token_endpoint_auth_method->value.string.p, + "client_secret_basic") == 0) { + result = "client_secret_basic"; + return result; + } + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_token_endpoint_auth: unsupported client auth method \"%s\" in client metadata for entry \"token_endpoint_auth_method\"", + token_endpoint_auth_method->value.string.p); + } else { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_metadata_token_endpoint_auth: unexpected JSON object type [%d] (!= APR_JSON_STRING) in client metadata for entry \"token_endpoint_auth_method\"", + token_endpoint_auth_method->type); + } + } + + /* no supported value in the client metadata, find a supported one in the provider metadata */ + apr_json_value_t *j_token_endpoint_auth_methods_supported = apr_hash_get( + j_provider->value.object, "token_endpoint_auth_methods_supported", + APR_HASH_KEY_STRING); + + if ((j_token_endpoint_auth_methods_supported != NULL) + && (j_token_endpoint_auth_methods_supported->type == APR_JSON_ARRAY)) { + int i; + for (i = 0; + i < j_token_endpoint_auth_methods_supported->value.array->nelts; + i++) { + apr_json_value_t *elem = APR_ARRAY_IDX( + j_token_endpoint_auth_methods_supported->value.array, i, + apr_json_value_t *); + if (elem->type != APR_JSON_STRING) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_metadata_token_endpoint_auth: unhandled in-array JSON object type [%d] in provider metadata for entry \"token_endpoint_auth_methods_supported\"", + elem->type); + continue; + } + if (strcmp(elem->value.string.p, "client_secret_post") == 0) { + result = "client_secret_post"; + break; + } + if (strcmp(elem->value.string.p, "client_secret_basic") == 0) { + result = "client_secret_basic"; + break; + } + } + } + + return result; +} + +/* + * get the metadata for a specified issuer + * + * this fill the oidc_op_meta_t struct based on the issuer filename by reading and merging + * contents from both provider metadata directory and client metadata directory + */ +apr_byte_t oidc_metadata_get(request_rec *r, oidc_cfg *cfg, const char *issuer, + oidc_provider_t **result) { + + /* pointer to the parsed JSON metadata for the provider */ + apr_json_value_t *j_provider = NULL; + /* pointer to the parsed JSON metadata for the client */ + apr_json_value_t *j_client = NULL; + + /* get the provider and client metadata */ + if (oidc_metadata_get_provider_and_client(r, cfg, issuer, &j_provider, + &j_client) == FALSE) + return FALSE; + + /* allocate space for a parsed-and-merged metadata struct */ + *result = apr_pcalloc(r->pool, sizeof(oidc_provider_t)); + /* provide easy pointer */ + oidc_provider_t *provider = *result; + + // PROVIDER + + /* get the "issuer" from the provider metadata and double-check that it matches what we looked for */ + apr_json_value_t *j_issuer = apr_hash_get(j_provider->value.object, + "issuer", APR_HASH_KEY_STRING); + + /* get a handle to the authorization endpoint */ + apr_json_value_t *j_authorization_endpoint = apr_hash_get( + j_provider->value.object, "authorization_endpoint", + APR_HASH_KEY_STRING); + + /* get a handle to the token endpoint */ + apr_json_value_t *j_token_endpoint = apr_hash_get(j_provider->value.object, + "token_endpoint", APR_HASH_KEY_STRING); + + /* get a handle to the user_info endpoint */ + apr_json_value_t *j_userinfo_endpoint = apr_hash_get( + j_provider->value.object, "userinfo_endpoint", APR_HASH_KEY_STRING); + + /* get a handle to the jwks_uri endpoint */ + apr_json_value_t *j_jwks_uri = apr_hash_get(j_provider->value.object, + "jwks_uri", APR_HASH_KEY_STRING); + + /* get the flow to use, client defined takes priority over provider defined */ + const char *response_type = cfg->provider.response_type; + + /* this is an array as by spec but we'll default to the first element */ + apr_json_value_t *j_response_types = apr_hash_get(j_client->value.object, + "response_types", APR_HASH_KEY_STRING); + if ((j_response_types != NULL) + && (j_response_types->type == APR_JSON_ARRAY)) { + apr_json_value_t *j_response_type = APR_ARRAY_IDX( + j_response_types->value.array, 0, apr_json_value_t *); + if (j_response_type->type == APR_JSON_STRING) { + response_type = j_response_type->value.string.p; + } + } + + /* put whatever we've found out about the provider in (the provider part of) the metadata struct */ + provider->issuer = apr_pstrdup(r->pool, j_issuer->value.string.p); + provider->authorization_endpoint_url = apr_pstrdup(r->pool, + j_authorization_endpoint->value.string.p); + if (j_token_endpoint != NULL) + provider->token_endpoint_url = apr_pstrdup(r->pool, + j_token_endpoint->value.string.p); + provider->token_endpoint_auth = apr_pstrdup(r->pool, + oidc_metadata_token_endpoint_auth(r, j_client, j_provider)); + if (j_userinfo_endpoint != NULL) + provider->userinfo_endpoint_url = apr_pstrdup(r->pool, + j_userinfo_endpoint->value.string.p); + if (j_jwks_uri != NULL) + provider->jwks_uri = apr_pstrdup(r->pool, j_jwks_uri->value.string.p); + provider->response_type = apr_pstrdup(r->pool, response_type); + + // CLIENT + + /* get a handle to the client_id we need to use for this provider */ + apr_json_value_t *j_client_id = apr_hash_get(j_client->value.object, + "client_id", APR_HASH_KEY_STRING); + + /* get a handle to the client_secret we need to use for this provider */ + apr_json_value_t *j_client_secret = apr_hash_get(j_client->value.object, + "client_secret", APR_HASH_KEY_STRING); + + /* find out if we need to perform SSL server certificate validation on the token_endpoint and user_info_endpoint for this provider */ + int validate = cfg->provider.ssl_validate_server; + apr_json_value_t *j_ssl_validate_server = apr_hash_get( + j_client->value.object, "ssl_validate_server", APR_HASH_KEY_STRING); + if ((j_ssl_validate_server != NULL) + && (j_ssl_validate_server->type == APR_JSON_STRING) + && (strcmp(j_ssl_validate_server->value.string.p, "Off") == 0)) { + validate = 0; + } + + /* find out what scopes we should be requesting from this provider */ + // TODO: use the provider "scopes_supported" to mix-and-match with what we've configured for the client + // TODO: check that "openid" is always included in the configured scopes, right? + const char *scope = cfg->provider.scope; + apr_json_value_t *j_scope = apr_hash_get(j_client->value.object, "scope", + APR_HASH_KEY_STRING); + if ((j_scope != NULL) && (j_scope->type == APR_JSON_STRING)) { + scope = j_scope->value.string.p; + } + + /* see if we've got a custom JWKs refresh interval */ + int jwks_refresh_interval = cfg->provider.jwks_refresh_interval; + apr_json_value_t *j_jwks_refresh_interval = apr_hash_get(j_client->value.object, "jwks_refresh_interval", + APR_HASH_KEY_STRING); + if ((j_jwks_refresh_interval != NULL) && (j_jwks_refresh_interval->type == APR_JSON_LONG)) { + jwks_refresh_interval = j_jwks_refresh_interval->value.lnumber; + } + + /* see if we've got a custom IAT slack interval */ + int idtoken_iat_slack = cfg->provider.idtoken_iat_slack; + apr_json_value_t *j_idtoken_iat_slack = apr_hash_get(j_client->value.object, "idtoken_iat_slack", + APR_HASH_KEY_STRING); + if ((j_idtoken_iat_slack != NULL) && (j_idtoken_iat_slack->type == APR_JSON_LONG)) { + idtoken_iat_slack = j_idtoken_iat_slack->value.lnumber; + } + + /* put whatever we've found out about the provider in (the client part of) the metadata struct */ + provider->ssl_validate_server = validate; + provider->client_id = apr_pstrdup(r->pool, j_client_id->value.string.p); + provider->client_secret = apr_pstrdup(r->pool, + j_client_secret->value.string.p); + provider->scope = apr_pstrdup(r->pool, scope); + provider->jwks_refresh_interval = jwks_refresh_interval; + provider->idtoken_iat_slack = idtoken_iat_slack; + + return TRUE; +} + diff --git a/src/mod_auth_openidc.c b/src/mod_auth_openidc.c new file mode 100644 index 00000000..6f8ee8cd --- /dev/null +++ b/src/mod_auth_openidc.c @@ -0,0 +1,1287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Initially based on mod_auth_cas.c: + * https://github.com/Jasig/mod_auth_cas + * + * Other code copied/borrowed/adapted: + * JSON decoding: apr_json.h apr_json_decode.c: https://github.com/moriyoshi/apr-json/ + * AES crypto: http://saju.net.in/code/misc/openssl_aes.c.txt + * session handling: Apache 2.4 mod_session.c + * session handling backport: http://contribsoft.caixamagica.pt/browser/internals/2012/apachecc/trunk/mod_session-port/src/util_port_compat.c + * shared memory caching: mod_auth_mellon + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + * + **************************************************************************/ + +#include "apr_hash.h" +#include "apr_strings.h" +#include "ap_config.h" +#include "ap_provider.h" +#include "apr_lib.h" +#include "apr_file_io.h" +#include "apr_sha1.h" +#include "apr_base64.h" + +#include "httpd.h" +#include "http_core.h" +#include "http_config.h" +#include "http_log.h" +#include "http_protocol.h" +#include "http_request.h" + +#include "mod_auth_openidc.h" + +// TODO: rigid input checking on discovery responses and authorization responses + +// TODO: use oidc_get_current_url + configured RedirectURIPath to determine the RedirectURI more dynamically +// TODO: support more hybrid flows ("code id_token" (for MS), "code token" etc.) +// TODO: support PS??? and EC??? algorithms +// TODO: override more stuff (eg. client_name, id_token_signed_response_alg) using client metadata + +// TODO: do we always want to refresh keys when signature does not validate? (risking DOS attacks, or does the nonce help against that?) +// do we now still want to refresh jkws once per hour (it helps to reduce the number of failed verifications, at the cost of too-many-downloads overhead) +// refresh metadata once-per too? (for non-signing key changes) +// TODO: check the Apache 2.4 compilation/#defines + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +/* + * clean any suspicious headers in the HTTP request sent by the user agent + */ +static void oidc_scrub_request_headers(request_rec *r, const char *claim_prefix, + const char *authn_header) { + + const int prefix_len = claim_prefix ? strlen(claim_prefix) : 0; + + /* get an array representation of the incoming HTTP headers */ + const apr_array_header_t * const h = apr_table_elts(r->headers_in); + + /* table to keep the non-suspicious headers */ + apr_table_t *clean_headers = apr_table_make(r->pool, h->nelts); + + /* loop over the incoming HTTP headers */ + const apr_table_entry_t * const e = (const apr_table_entry_t *) h->elts; + int i; + for (i = 0; i < h->nelts; i++) { + const char * const k = e[i].key; + + /* is this header's name equivalent to the header that mod_auth_openidc would set for the authenticated user? */ + const int authn_header_matches = (k != NULL) && authn_header + && (oidc_strnenvcmp(k, authn_header, -1) == 0); + + /* + * would this header be interpreted as a mod_auth_openidc attribute? Note + * that prefix_len will be zero if no attr_prefix is defined, + * so this will always be false. Also note that we do not + * scrub headers if the prefix is empty because every header + * would match. + */ + const int prefix_matches = (k != NULL) && prefix_len + && (oidc_strnenvcmp(k, claim_prefix, prefix_len) == 0); + + /* add to the clean_headers if non-suspicious, skip and report otherwise */ + if (!prefix_matches && !authn_header_matches) { + apr_table_addn(clean_headers, k, e[i].val); + } else { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_scrub_request_headers: scrubbed suspicious request header (%s: %.32s)", + k, e[i].val); + } + } + + /* overwrite the incoming headers with the cleaned result */ + r->headers_in = clean_headers; +} + +/* + * calculates a hash value based on request fingerprint plus a provided state string. + */ +static char *oidc_get_browser_state_hash(request_rec *r, const char *state) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_get_browser_state_hash: entering"); + + /* helper to hold to header values */ + const char *value = NULL; + /* the hash context */ + apr_sha1_ctx_t sha1; + + /* Initialize the hash context */ + apr_sha1_init(&sha1); + + /* get the X_FORWARDED_FOR header value */ + value = (char *) apr_table_get(r->headers_in, "X_FORWARDED_FOR"); + /* if we have a value for this header, concat it to the hash input */ + if (value != NULL) + apr_sha1_update(&sha1, value, strlen(value)); + + /* get the USER_AGENT header value */ + value = (char *) apr_table_get(r->headers_in, "USER_AGENT"); + /* if we have a value for this header, concat it to the hash input */ + if (value != NULL) + apr_sha1_update(&sha1, value, strlen(value)); + + /* get the remote client IP address or host name */ + int remotehost_is_ip; + value = ap_get_remote_host(r->connection, r->per_dir_config, + REMOTE_NOLOOKUP, &remotehost_is_ip); + /* concat the remote IP address/hostname to the hash input */ + apr_sha1_update(&sha1, value, strlen(value)); + + /* concat the state parameter to the hash input */ + apr_sha1_update(&sha1, state, strlen(state)); + + /* finalize the hash input and calculate the resulting hash output */ + const int sha1_len = 20; + unsigned char hash[sha1_len]; + apr_sha1_final(hash, &sha1); + + /* base64 encode the resulting hash and return it */ + char *result = apr_palloc(r->pool, apr_base64_encode_len(sha1_len) + 1); + apr_base64_encode(result, (const char *) hash, sha1_len); + return result; +} + +/* + * see if the state that came back from the OP matches what we've stored in the cookie + */ +static int oidc_check_state(request_rec *r, oidc_cfg *c, const char *state, + char **original_url, char **issuer, char **nonce) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_check_state: entering"); + + /* get the state cookie value first */ + char *cookieValue = oidc_get_cookie(r, OIDCStateCookieName); + if (cookieValue == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: no \"%s\" state cookie found", + OIDCStateCookieName); + return FALSE; + } + + /* clear state cookie because we don't need it anymore */ + oidc_set_cookie(r, OIDCStateCookieName, ""); + + /* decrypt the state obtained from the cookie */ + char *svalue; + if (oidc_base64url_decode_decrypt_string(r, &svalue, cookieValue) <= 0) + return FALSE; + + /* context to iterate over the entries in the decrypted state cookie value */ + char *ctx = NULL; + + /* first get the base64-encoded random value */ + *nonce = apr_strtok(svalue, OIDCStateCookieSep, &ctx); + if (*nonce == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: no nonce element found in \"%s\" cookie (%s)", + OIDCStateCookieName, cookieValue); + return FALSE; + } + + /* calculate the hash of the browser fingerprint concatenated with the nonce */ + char *calc = oidc_get_browser_state_hash(r, *nonce); + + /* compare the calculated hash with the value provided in the authorization response */ + if (apr_strnatcmp(calc, state) != 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: calculated state from cookie does not match state parameter passed back in URL: \"%s\" != \"%s\"", + state, calc); + return FALSE; + } + + /* since we're OK, get the original URL as the next value in the decrypted cookie */ + *original_url = apr_strtok(NULL, OIDCStateCookieSep, &ctx); + if (*original_url == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: no separator (%s) found in \"%s\" cookie (%s)", + OIDCStateCookieSep, OIDCStateCookieName, cookieValue); + return FALSE; + } + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_check_state: \"original_url\" restored from cookie: %s", + *original_url); + + /* thirdly, get the issuer value stored in the cookie */ + *issuer = apr_strtok(NULL, OIDCStateCookieSep, &ctx); + if (*issuer == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: no second separator (%s) found in \"%s\" cookie (%s)", + OIDCStateCookieSep, OIDCStateCookieName, cookieValue); + return FALSE; + } + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_check_state: \"issuer\" restored from cookie: %s", *issuer); + + /* lastly, get the timestamp value stored in the cookie */ + char *timestamp = apr_strtok(NULL, OIDCStateCookieSep, &ctx); + if (timestamp == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: no third separator (%s) found in \"%s\" cookie (%s)", + OIDCStateCookieSep, OIDCStateCookieName, cookieValue); + return FALSE; + } + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_check_state: \"timestamp\" restored from cookie: %s", + timestamp); + + apr_time_t then; + if (sscanf(timestamp, "%" APR_TIME_T_FMT, &then) != 1) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: could not parse timestamp restored from state cookie (%s)", + timestamp); + return FALSE; + } + + apr_time_t now = apr_time_sec(apr_time_now()); + if (now > then + c->state_timeout) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_check_state: state has expired"); + return FALSE; + } + + /* we've made it */ + return TRUE; +} + +/* + * create a state parameter to be passed in an authorization request to an OP + * and set a cookie in the browser that is cryptographically bound to that + */ +static char *oidc_create_state_and_set_cookie(request_rec *r, const char *url, + const char *issuer, const char *nonce) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_create_state_and_set_cookie: entering"); + + char *cookieValue = NULL; + + /* + * create a cookie consisting of 4 elements: + * random value, original URL, issuer and timestamp separated by a defined separator + */ + apr_time_t now = apr_time_sec(apr_time_now()); + char *rvalue = apr_psprintf(r->pool, "%s%s%s%s%s%s%" APR_TIME_T_FMT "", + nonce, + OIDCStateCookieSep, url, OIDCStateCookieSep, issuer, + OIDCStateCookieSep, now); + + /* encrypt the resulting value and set it as a cookie */ + oidc_encrypt_base64url_encode_string(r, &cookieValue, rvalue); + oidc_set_cookie(r, OIDCStateCookieName, cookieValue); + + /* return a hash value that fingerprints the browser concatenated with the random input */ + return oidc_get_browser_state_hash(r, nonce); +} + +/* + * get the mod_auth_openidc related context from the (userdata in the) request + * (used for passing state between various Apache request processing stages and hook callbacks) + */ +static apr_table_t *oidc_request_state(request_rec *rr) { + + /* our state is always stored in the main request */ + request_rec *r = (rr->main != NULL) ? rr->main : rr; + + /* our state is a table, get it */ + apr_table_t *state = NULL; + apr_pool_userdata_get((void **) &state, OIDC_USERDATA_KEY, r->pool); + + /* if it does not exist, we'll create a new table */ + if (state == NULL) { + state = apr_table_make(r->pool, 5); + apr_pool_userdata_set(state, OIDC_USERDATA_KEY, NULL, r->pool); + } + + /* return the resulting table, always non-null now */ + return state; +} + +/* + * set a name/value pair in the mod_auth_openidc-specific request context + * (used for passing state between various Apache request processing stages and hook callbacks) + */ +void oidc_request_state_set(request_rec *r, const char *key, const char *value) { + + /* get a handle to the global state, which is a table */ + apr_table_t *state = oidc_request_state(r); + + /* put the name/value pair in that table */ + apr_table_setn(state, key, value); +} + +/* + * get a name/value pair from the mod_auth_openidc-specific request context + * (used for passing state between various Apache request processing stages and hook callbacks) + */ +const char*oidc_request_state_get(request_rec *r, const char *key) { + + /* get a handle to the global state, which is a table */ + apr_table_t *state = oidc_request_state(r); + + /* return the value from the table */ + return apr_table_get(state, key); +} + +/* + * set an HTTP header to pass information to the application + */ +static void oidc_set_app_header(request_rec *r, const char *s_key, + const char *s_value, const char *claim_prefix) { + + /* construct the header name, cq. put the prefix in front of a normalized key name */ + const char *s_name = apr_psprintf(r->pool, "%s%s", claim_prefix, + oidc_normalize_header_name(r, s_key)); + + /* do some logging about this event */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_set_app_header: setting header \"%s: %s\"", s_name, s_value); + + /* now set the actual header name/value */ + apr_table_set(r->headers_in, s_name, s_value); +} + +/* + * set the user/claims information from the session in HTTP headers passed on to the application + */ +static void oidc_set_app_headers(request_rec *r, + const apr_json_value_t *j_attrs, const char *authn_header, + const char *claim_prefix, const char *claim_delimiter) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_set_app_headers: entering"); + + apr_json_value_t *j_value = NULL; + apr_hash_index_t *hi = NULL; + const char *s_key = NULL; + + /* set the user authentication HTTP header if set and required */ + if ((r->user != NULL) && (authn_header != NULL)) + apr_table_set(r->headers_in, authn_header, r->user); + + /* if not attributes are set, nothing needs to be done */ + if (j_attrs == NULL) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_set_app_headers: no attributes to set (j_attrs=NULL)"); + return; + } + + /* loop over the claims in the JSON structure */ + for (hi = apr_hash_first(r->pool, j_attrs->value.object); hi; hi = + apr_hash_next(hi)) { + + /* get the next key/value entry */ + apr_hash_this(hi, (const void**) &s_key, NULL, (void**) &j_value); + + /* check if it is a single value string */ + if (j_value->type == APR_JSON_STRING) { + + /* set the single string in the application header whose name is based on the key and the prefix */ + oidc_set_app_header(r, s_key, j_value->value.string.p, + claim_prefix); + + } else if (j_value->type == APR_JSON_BOOLEAN) { + + /* set boolean value in the application header whose name is based on the key and the prefix */ + oidc_set_app_header(r, s_key, j_value->value.boolean ? "1" : "0", + claim_prefix); + + } else if (j_value->type == APR_JSON_LONG) { + + /* set long value in the application header whose name is based on the key and the prefix */ + oidc_set_app_header(r, s_key, + apr_psprintf(r->pool, "%ld", j_value->value.lnumber), + claim_prefix); + + } else if (j_value->type == APR_JSON_DOUBLE) { + + /* set float value in the application header whose name is based on the key and the prefix */ + oidc_set_app_header(r, s_key, + apr_psprintf(r->pool, "%lf", j_value->value.dnumber), + claim_prefix); + + /* check if it is a multi-value string */ + } else if (j_value->type == APR_JSON_ARRAY) { + + /* some logging about what we're going to do */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_set_app_headers: parsing attribute array for key \"%s\" (#nr-of-elems: %d)", + s_key, j_value->value.array->nelts); + + /* string to hold the concatenated array string values */ + char *s_concat = apr_pstrdup(r->pool, ""); + int i = 0; + + /* loop over the array */ + for (i = 0; i < j_value->value.array->nelts; i++) { + + /* get the current element */ + apr_json_value_t *elem = APR_ARRAY_IDX(j_value->value.array, i, + apr_json_value_t *); + + /* check if it is a string */ + if (elem->type == APR_JSON_STRING) { + + /* concatenate the string to the s_concat value using the configured separator char */ + // TODO: escape the delimiter in the values (maybe reuse/extract url-formatted code from oidc_session_identity_encode) + if (apr_strnatcmp(s_concat, "") != 0) { + s_concat = apr_psprintf(r->pool, "%s%s%s", s_concat, + claim_delimiter, elem->value.string.p); + } else { + s_concat = apr_psprintf(r->pool, "%s", + elem->value.string.p); + } + + } else if (elem->type == APR_JSON_BOOLEAN) { + + if (apr_strnatcmp(s_concat, "") != 0) { + s_concat = apr_psprintf(r->pool, "%s%s%s", s_concat, + claim_delimiter, + j_value->value.boolean ? "1" : "0"); + } else { + s_concat = apr_psprintf(r->pool, "%s", + j_value->value.boolean ? "1" : "0"); + } + + } else { + + /* don't know how to handle a non-string array element */ + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_set_app_headers: unhandled in-array JSON object type [%d] for key \"%s\" when parsing claims array elements", + elem->type, s_key); + } + } + + /* set the concatenated string */ + oidc_set_app_header(r, s_key, s_concat, claim_prefix); + + } else { + + /* no string and no array, so unclear how to handle this */ + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_set_app_headers: unhandled JSON object type [%d] for key \"%s\" when parsing claims", + j_value->type, s_key); + } + } +} + +/* + * handle the case where we have identified an existing authentication session for a user + */ +static int oidc_handle_existing_session(request_rec *r, + const oidc_cfg * const cfg, session_rec *session) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_handle_existing_session: entering"); + + const char *s_attrs = NULL; + apr_json_value_t *j_attrs = NULL; + + /* get a handle to the director config */ + oidc_dir_cfg *dir_cfg = ap_get_module_config(r->per_dir_config, + &auth_openidc_module); + + /* + * we're going to pass the information that we have to the application, + * but first we need to scrub the headers that we're going to use for security reasons + */ + if (cfg->scrub_request_headers != 0) { + oidc_scrub_request_headers(r, cfg->claim_prefix, dir_cfg->authn_header); + } + + /* get the string-encoded attributes from the session */ + oidc_session_get(r, session, OIDC_CLAIMS_SESSION_KEY, &s_attrs); + + /* decode the string-encoded attributes in to a JSON structure */ + if ((s_attrs != NULL) + && (apr_json_decode(&j_attrs, s_attrs, strlen(s_attrs), r->pool) + != APR_SUCCESS)) { + + /* whoops, attributes have been corrupted */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_handle_existing_session: unable to parse string-encoded claims stored in the session, returning internal server error"); + + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* pass the user/claims in the session to the application by setting the appropriate headers */ + // TODO: combine already resolved attrs from id_token with those from user_info endpoint + oidc_set_app_headers(r, j_attrs, dir_cfg->authn_header, cfg->claim_prefix, + cfg->claim_delimiter); + + /* set the attributes JSON structure in the request state so it is available for authz purposes later on */ + oidc_request_state_set(r, OIDC_CLAIMS_SESSION_KEY, (const char *) j_attrs); + + /* return "user authenticated" status */ + return OK; +} + +/* + * helper function for basic/implicit client flows upon receiving an authorization response: + * check that it matches the state stored in the browser and return the variables associated + * with the state, such as original_url and OP oidc_provider_t pointer. + */ +static apr_byte_t oidc_authorization_response_match_state(request_rec *r, + oidc_cfg *c, const char *state, char **original_url, + struct oidc_provider_t **provider, char **nonce) { + char *issuer = NULL; + + /* check the state parameter against what we stored in a cookie */ + if (oidc_check_state(r, c, state, original_url, &issuer, nonce) == FALSE) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_authorization_response_match_state: unable to restore state"); + return FALSE; + } + + /* by default we'll assume that we're dealing with a single statically configured OP */ + *provider = &c->provider; + + /* unless a metadata directory was configured, so we'll try and get the provider settings from there */ + if (c->metadata_dir != NULL) { + + /* try and get metadata from the metadata directory for the OP that sent this response */ + if ((oidc_metadata_get(r, c, issuer, provider) == FALSE) + || (provider == NULL)) { + + // something went wrong here between sending the request and receiving the response + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_authorization_response_match_state: no provider metadata found for provider \"%s\"", + issuer); + return FALSE; + } + } + + return TRUE; +} + +/* + * helper function for basic/implicit client flows: + * complete the handling of an authorization response by storing the + * authenticated user state in the session + */ +static int oidc_authorization_response_finalize(request_rec *r, oidc_cfg *c, + session_rec *session, const char *id_token, const char *claims, + char *remoteUser, apr_time_t expires, const char *original_url) { + + /* set the resolved stuff in the session */ + session->remote_user = remoteUser; + + /* expires is the value from the id_token */ + session->expiry = expires; + + /* store the whole contents of the id_token for later reference too */ + oidc_session_set(r, session, OIDC_IDTOKEN_SESSION_KEY, id_token); + + /* see if we've resolved any claims */ + if (claims != NULL) { + /* + * Successfully decoded a set claims from the response so we can store them + * (well actually the stringified representation in the response) + * in the session context safely now + */ + oidc_session_set(r, session, OIDC_CLAIMS_SESSION_KEY, claims); + } + + /* store the session */ + oidc_session_save(r, session); + + /* not sure whether this is required, but it won't hurt */ + r->user = remoteUser; + + /* now we've authenticated the user so go back to the URL that he originally tried to access */ + apr_table_add(r->headers_out, "Location", original_url); + + /* log the successful response */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authorization_response_finalize: session created and stored, redirecting to original url: %s", + original_url); + + /* do the actual redirect to the original URL */ + return HTTP_MOVED_TEMPORARILY; +} + +/* + * handle an OpenID Connect Authorization Response using the Basic Client profile from the OP + */ +static int oidc_handle_basic_authorization_response(request_rec *r, oidc_cfg *c, + session_rec *session) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_handle_basic_authorization_response: entering"); + + /* initialize local variables */ + char *code = NULL, *state = NULL; + + /* by now we're pretty sure the code & state parameters exist */ + oidc_util_get_request_parameter(r, "code", &code); + oidc_util_get_request_parameter(r, "state", &state); + + /* match the returned state parameter against the state stored in the browser */ + struct oidc_provider_t *provider = NULL; + char *original_url = NULL; + char *nonce = NULL; + if (oidc_authorization_response_match_state(r, c, state, &original_url, + &provider, &nonce) == FALSE) + return HTTP_INTERNAL_SERVER_ERROR; + + /* now we've got the metadata for the provider that sent the response to us */ + char *id_token = NULL, *access_token = NULL; + const char *response = NULL; + char *remoteUser = NULL; + apr_time_t expires; + apr_json_value_t *j_idtoken_payload = NULL; + + /* + * resolve the code against the token endpoint of the OP + * TODO: now I'm setting the nonce to NULL since google does not allow using a nonce in the "code" flow... + */ + nonce = NULL; + if (oidc_proto_resolve_code(r, c, provider, code, nonce, &remoteUser, + &j_idtoken_payload, &id_token, &access_token, &expires) == FALSE) { + /* errors have already been reported */ + return HTTP_UNAUTHORIZED; + } + + /* + * optionally resolve additional claims against the userinfo endpoint + * parsed claims are not actually used here but need to be parsed anyway for error checking purposes + */ + apr_json_value_t *claims = NULL; + if (oidc_proto_resolve_userinfo(r, c, provider, access_token, &response, + &claims) == FALSE) { + response = NULL; + } + + /* complete handling of the response by storing stuff in the session and redirecting to the original URL */ + return oidc_authorization_response_finalize(r, c, session, id_token, + response, remoteUser, expires, original_url); +} + +/* + * handle an OpenID Connect Authorization Response using the Implicit Client profile from the OP + */ +static int oidc_handle_implicit_authorization_response(request_rec *r, + oidc_cfg *c, session_rec *session, const char *state, + const char *id_token, const char *access_token, const char *token_type) { + + /* log what we've received */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_handle_implicit_authorization_response: state = \"%s\", id_token= \"%s\", access_token=\"%s\", token_type=\"%s\"", + state, id_token, access_token, token_type); + + /* match the returned state parameter against the state stored in the browser */ + struct oidc_provider_t *provider = NULL; + char *original_url = NULL; + char *nonce = NULL; + if (oidc_authorization_response_match_state(r, c, state, &original_url, + &provider, &nonce) == FALSE) + return HTTP_INTERNAL_SERVER_ERROR; + + /* initialize local variables for the id_token contents */ + char *remoteUser = NULL; + apr_json_value_t *j_idtoken_payload = NULL; + apr_time_t expires; + char *s_idtoken_payload = NULL; + + /* parse and validate the id_token */ + if (oidc_proto_parse_idtoken(r, c, provider, id_token, nonce, &remoteUser, + &j_idtoken_payload, &s_idtoken_payload, &expires) != TRUE) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_handle_implicit_authorization_response: could not verify the id_token contents, return HTTP_UNAUTHORIZED"); + return HTTP_UNAUTHORIZED; + } + + // TODO: all id_token stuff in/as claims, should probably filter...? + + const char *s_claims = s_idtoken_payload; + + /* strip empty parameters (eg. connect.openid4.us on response on "id_token" flow) */ + if ((access_token != NULL) && (strcmp(access_token, "") == 0)) + access_token = NULL; + + /* assert that the token_type is Bearer before using it */ + if ((token_type != NULL) && (strcmp(token_type, "") != 0)) { + if (apr_strnatcasecmp(token_type, "Bearer") != 0) { + ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, + "oidc_handle_implicit_authorization_response: dropping unsupported (cq. non \"Bearer\") token_type: \"%s\"", + token_type); + access_token = NULL; + } + } + + /* + * if we (still) have an access_token, let's use to resolve claims from the user_info endpoint + * we don't do anything with the optional expires_in, since we don't cache the access_token or re-use + * it in any way after this initial call that should happen right after issuing the access_token + * (and it is optional anyway) + */ + if (access_token != NULL) { + + /* parsed claims are not actually used here but need to be parsed anyway for error checking purposes */ + apr_json_value_t *claims = NULL; + if (oidc_proto_resolve_userinfo(r, c, provider, access_token, &s_claims, + &claims) == FALSE) { + s_claims = NULL; + } + } + + /* complete handling of the response by storing stuff in the session and redirecting to the original URL */ + return oidc_authorization_response_finalize(r, c, session, id_token, + s_claims, remoteUser, expires, original_url); +} + +/* + * handle an OpenID Connect Authorization Response using the fragment(+POST) response_mode with the Implicit Client profile from the OP + */ +static int oidc_handle_implicit_post(request_rec *r, oidc_cfg *c, + session_rec *session) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_handle_implicit_post: entering"); + + /* read the parameters that are POST-ed to us */ + apr_table_t *params = apr_table_make(r->pool, 8); + if (oidc_util_read_post(r, params) == FALSE) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_handle_implicit_post: something went wrong when reading the POST parameters"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* see if we've got any POST-ed data at all */ + if (apr_is_empty_table(params)) { + return oidc_util_http_sendstring(r, + apr_psprintf(r->pool, + "mod_auth_openidc: you've hit an OpenID Connect callback URL with no parameters; this is an invalid request (you should not open this URL in your browser directly)"), + HTTP_INTERNAL_SERVER_ERROR); + } + + /* see if the response is an error response */ + char *error = (char *) apr_table_get(params, "error"); + char *error_description = (char *) apr_table_get(params, + "error_description"); + if (error != NULL) + return oidc_util_html_send_error(r, error, error_description, OK); + + /* get the state */ + char *state = (char *) apr_table_get(params, "state"); + if (state == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_handle_implicit_post: no state parameter found in the POST, returning internal server error"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* get the id_token */ + char *id_token = (char *) apr_table_get(params, "id_token"); + if (id_token == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_handle_implicit_post: no id_token parameter found in the POST, returning internal server error"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + /* get the (optional) access_token */ + char *access_token = (char *) apr_table_get(params, "access_token"); + + /* get the (optional) token_type */ + char *token_type = (char *) apr_table_get(params, "token_type"); + + /* do the actual implicit work */ + return oidc_handle_implicit_authorization_response(r, c, session, state, + id_token, access_token, token_type); +} + +/* + * handle an OpenID Connect Authorization Response using the redirect response_mode with the Implicit Client profile from the OP + */ +static int oidc_handle_implicit_redirect(request_rec *r, oidc_cfg *c, + session_rec *session) { + + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_handle_implicit_redirect: handling non-spec-compliant authorization response since the default response_mode when using the Implicit Client flow must be \"fragment\""); + + /* initialize local variables */ + char *state = NULL, *id_token = NULL, *access_token = NULL, *token_type = + NULL; + + /* by now we're pretty sure the state & id_token parameters exist */ + oidc_util_get_request_parameter(r, "state", &state); + oidc_util_get_request_parameter(r, "id_token", &id_token); + oidc_util_get_request_parameter(r, "access_token", &access_token); + oidc_util_get_request_parameter(r, "token_type", &token_type); + + /* do the actual implicit work */ + return oidc_handle_implicit_authorization_response(r, c, session, state, + id_token, access_token, token_type); +} + +/* + * present the user with an OP selection screen + */ +static int oidc_discovery(request_rec *r, oidc_cfg *cfg) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_discovery: entering"); + + /* obtain the URL we're currently accessing, to be stored in the state/session */ + char *current_url = oidc_get_current_url(r, cfg); + + /* see if there's an external discovery page configured */ + if (cfg->discover_url != NULL) { + + /* yes, assemble the parameters for external discovery */ + char *url = apr_psprintf(r->pool, "%s%s%s=%s&%s=%s", cfg->discover_url, + strchr(cfg->discover_url, '?') != NULL ? "&" : "?", + OIDC_DISC_RT_PARAM, oidc_util_escape_string(r, current_url), + OIDC_DISC_CB_PARAM, + oidc_util_escape_string(r, cfg->redirect_uri)); + + /* log what we're about to do */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_discovery: redirecting to external discovery page: %s", + url); + + /* do the actual redirect to an external discovery page */ + apr_table_add(r->headers_out, "Location", url); + return HTTP_MOVED_TEMPORARILY; + } + + /* get a list of all providers configured in the metadata directory */ + apr_array_header_t *arr = NULL; + if (oidc_metadata_list(r, cfg, &arr) == FALSE) + return oidc_util_http_sendstring(r, + "mod_auth_openidc: no configured providers found, contact your administrator", + HTTP_UNAUTHORIZED); + + /* assemble a where-are-you-from IDP discovery HTML page */ + // TODO: yes, we could use some templating here... + const char *s = + "" + "\n" + " \n" + " \n" + " OpenID Connect Provider Discovery\n" + " \n" + " \n" + "
\n" + "

Select your OpenID Connect Identity Provider

\n"; + + /* list all configured providers in there */ + int i; + for (i = 0; i < arr->nelts; i++) { + const char *issuer = ((const char**) arr->elts)[i]; + // TODO: html escape (especially & character) + + char *display = + (strstr(issuer, "https://") == NULL) ? + apr_pstrdup(r->pool, issuer) : + apr_pstrdup(r->pool, issuer + strlen("https://")); + + /* strip port number */ + //char *p = strstr(display, ":"); + //if (p != NULL) *p = '\0'; + /* point back to the redirect_uri, where the selection is handled, with an IDP selection and return_to URL */ + s = apr_psprintf(r->pool, + "%s

%s

\n", s, + cfg->redirect_uri, OIDC_DISC_OP_PARAM, + oidc_util_escape_string(r, issuer), OIDC_DISC_RT_PARAM, + oidc_util_escape_string(r, current_url), display); + } + + /* add an option to enter an account or issuer name for dynamic OP discovery */ + s = apr_psprintf(r->pool, "%s
\n", s, + cfg->redirect_uri); + s = apr_psprintf(r->pool, + "%s
\n", s, + OIDC_DISC_RT_PARAM, current_url); + s = + apr_psprintf(r->pool, + "%sOr enter your account name (eg. \"mike@seed.gluu.org\", or an IDP identifier (eg. \"mitreid.org\"):
\n", + s); + s = apr_psprintf(r->pool, + "%s

\n", s, + OIDC_DISC_OP_PARAM, ""); + s = apr_psprintf(r->pool, "%s\n", + s); + s = apr_psprintf(r->pool, "%s
\n", s); + + /* footer */ + s = apr_psprintf(r->pool, "%s" + "
\n" + " \n" + "\n", s); + + /* now send the HTML contents to the user agent */ + return oidc_util_http_sendstring(r, s, HTTP_UNAUTHORIZED); +} + +/* + * authenticate the user to the selected OP, if the OP is not selected yet perform discovery first + */ +static int oidc_authenticate_user(request_rec *r, oidc_cfg *c, + oidc_provider_t *provider, const char *original_url) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_authenticate_user: entering"); + + if (provider == NULL) { + + // TODO: should we use an explicit redirect to the discovery endpoint (maybe a "discovery" param to the redirect_uri)? + if (c->metadata_dir != NULL) + return oidc_discovery(r, c); + + /* we're not using multiple OP's configured in a metadata directory, pick the statically configured OP */ + provider = &c->provider; + } + + char *nonce = NULL; + oidc_util_generate_random_base64url_encoded_value(r, 32, &nonce); + + /* create state that restores the context when the authorization response comes in; cryptographically bind it to the browser */ + const char *state = oidc_create_state_and_set_cookie(r, original_url, + provider->issuer, nonce); + + // TODO: maybe show intermediate/progress screen "redirecting to" + + /* send off to the OpenID Connect Provider */ + return oidc_proto_authorization_request(r, provider, c->redirect_uri, state, + original_url, nonce); +} + +/* + * find out whether the request is a response from an IDP discovery page + */ +static apr_byte_t oidc_is_discovery_response(request_rec *r, oidc_cfg *cfg) { + /* + * see if this is a call to the configured redirect_uri and + * the OIDC_RT_PARAM_NAME parameter is present and + * the OIDC_DISC_ACCT_PARAM or OIDC_DISC_OP_PARAM is present + */ + return ((oidc_util_request_matches_url(r, cfg->redirect_uri) == TRUE) + && oidc_util_request_has_parameter(r, OIDC_DISC_RT_PARAM) + && (oidc_util_request_has_parameter(r, OIDC_DISC_OP_PARAM))); +} + +/* + * handle a response from an IDP discovery page + */ +static int oidc_handle_discovery_response(request_rec *r, oidc_cfg *c) { + + /* variables to hold the values (original_url+issuer or original_url+acct) returned in the response */ + char *issuer = NULL, *original_url = NULL; + oidc_provider_t *provider = NULL; + + oidc_util_get_request_parameter(r, OIDC_DISC_OP_PARAM, &issuer); + oidc_util_get_request_parameter(r, OIDC_DISC_RT_PARAM, &original_url); + + // TODO: trim issuer/accountname/domain input and do more input validation + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_handle_discovery_response: issuer=\"%s\", original_url=\"%s\"", + issuer, original_url); + + if ((issuer == NULL) || (original_url == NULL)) { + return oidc_util_http_sendstring(r, + "mod_auth_openidc: wherever you came from, it sent you here with the wrong parameters...", + HTTP_INTERNAL_SERVER_ERROR); + } + + /* find out if the user entered an account name or selected an OP manually */ + if (strstr(issuer, "@") != NULL) { + + /* got an account name as input, perform OP discovery with that */ + if (oidc_proto_account_based_discovery(r, c, issuer, &issuer) == FALSE) { + + /* something did not work out, show a user facing error */ + return oidc_util_http_sendstring(r, + "mod_auth_openidc: could not resolve the provided account name to an OpenID Connect provider; check your syntax", + HTTP_NOT_FOUND); + } + + /* issuer is set now, so let's continue as planned */ + + } else if (strcmp(issuer, "accounts.google.com") != 0) { + + /* allow issuer/domain entries that don't start with https */ + issuer = apr_psprintf(r->pool, "%s", + ((strstr(issuer, "http://") == issuer) + || (strstr(issuer, "https://") == issuer)) ? + issuer : apr_psprintf(r->pool, "https://%s", issuer)); + } + + /* strip trailing '/' */ + int n = strlen(issuer); + if (issuer[n - 1] == '/') + issuer[n - 1] = '\0'; + + /* try and get metadata from the metadata directories for the selected OP */ + if ((oidc_metadata_get(r, c, issuer, &provider) == TRUE) + && (provider != NULL)) { + + /* now we've got a selected OP, send the user there to authenticate */ + return oidc_authenticate_user(r, c, provider, original_url); + } + + /* something went wrong */ + return oidc_util_http_sendstring(r, + "mod_auth_openidc: could not find valid provider metadata for the selected OpenID Connect provider; contact the administrator", + HTTP_NOT_FOUND); +} + +/* + * handle "all other" requests to the redirect_uri + */ +int oidc_handle_redirect_uri_request(request_rec *r, oidc_cfg *c) { + if (r->args == NULL) + /* this is a "bare" request to the redirect URI, indicating implicit flow using the fragment response_mode */ + return oidc_proto_javascript_implicit(r, c); + + /* TODO: check for "error" response */ + if (oidc_util_request_has_parameter(r, "error")) { + + char *error = NULL, *descr = NULL; + oidc_util_get_request_parameter(r, "error", &error); + oidc_util_get_request_parameter(r, "error_description", &descr); + + return oidc_util_html_send_error(r, error, descr, OK); + } + + /* something went wrong */ + return oidc_util_http_sendstring(r, + apr_psprintf(r->pool, + "mod_auth_openidc: the OpenID Connect callback URL received an invalid request: %s", + r->args), HTTP_INTERNAL_SERVER_ERROR); +} + +/* + * kill session + */ +int oidc_handle_logout(request_rec *r, session_rec *session) { + char *url = NULL; + + /* if there's no remote_user then there's no (stored) session to kill */ + if (session->remote_user != NULL) { + + /* remove session state (cq. cache entry and cookie) */ + oidc_session_kill(r, session); + } + + /* pickup the URL where the user wants to go after logout */ + oidc_util_get_request_parameter(r, "logout", &url); + + /* send him there */ + apr_table_add(r->headers_out, "Location", url); + return HTTP_MOVED_TEMPORARILY; +} + +/* + * main routine: handle OpenID Connect authentication + */ +static int oidc_check_userid_openid_openidc(request_rec *r, oidc_cfg *c) { + + /* check if this is a sub-request or an initial request */ + if (ap_is_initial_req(r)) { + + /* load the session from the request state; this will be a new "empty" session if no state exists */ + session_rec *session = NULL; + oidc_session_load(r, &session); + + /* see if this is a logout trigger */ + if ((oidc_util_request_matches_url(r, c->redirect_uri) == TRUE) + && (oidc_util_request_has_parameter(r, "logout") == TRUE)) { + + /* handle logout */ + return oidc_handle_logout(r, session); + } + + /* initial request, first check if we have an existing session */ + if (session->remote_user != NULL) { + + /* set the user in the main request for further (incl. sub-request) processing */ + r->user = (char *) session->remote_user; + + /* this is initial request and we already have a session */ + return oidc_handle_existing_session(r, c, session); + + } else if (oidc_is_discovery_response(r, c)) { + + /* this is response from the OP discovery page */ + return oidc_handle_discovery_response(r, c); + + } else if (oidc_proto_is_basic_authorization_response(r, c)) { + + /* this is an authorization response from the OP using the Basic Client profile */ + return oidc_handle_basic_authorization_response(r, c, session); + + } else if (oidc_proto_is_implicit_post(r, c)) { + + /* this is an authorization response using the fragment(+POST) response_mode with the Implicit Client profile */ + return oidc_handle_implicit_post(r, c, session); + + } else if (oidc_proto_is_implicit_redirect(r, c)) { + + /* this is an authorization response using the redirect response_mode with the Implicit Client profile */ + return oidc_handle_implicit_redirect(r, c, session); + + } else if (oidc_util_request_matches_url(r, c->redirect_uri) == TRUE) { + + /* some other request to the redirect_uri */ + return oidc_handle_redirect_uri_request(r, c); + } + /* + * else: initial request, we have no session and it is not an authorization or + * discovery response: just hit the default flow for unauthenticated users + */ + } else { + + /* not an initial request, try to recycle what we've already established in the main request */ + if (r->main != NULL) + r->user = r->main->user; + else if (r->prev != NULL) + r->user = r->prev->user; + + if (r->user != NULL) { + + /* this is a sub-request and we have a session (headers will have been scrubbed and set already) */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_check_userid_openid_openidc: recycling user '%s' from initial request for sub-request", + r->user); + + return OK; + } + /* + * else: not initial request, but we could not find a session, so: + * just hit the default flow for unauthenticated users + */ + } + + /* no session (regardless of whether it is main or sub-request), go and authenticate the user */ + return oidc_authenticate_user(r, c, NULL, oidc_get_current_url(r, c)); +} + +/* + * generic Apache authentication hook for this module: dispatches to OpenID Connect or OAuth 2.0 specific routines + */ +int oidc_check_user_id(request_rec *r) { + + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + + /* log some stuff about the incoming HTTP request */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_check_user_id: incoming request: \"%s?%s\", ap_is_initial_req(r)=%d", + r->parsed_uri.path, r->args, ap_is_initial_req(r)); + + /* see if any authentication has been defined at all */ + if (ap_auth_type(r) == NULL) + return DECLINED; + + /* see if we've configured OpenID Connect user authentication for this request */ + if (apr_strnatcasecmp((const char *) ap_auth_type(r), "openid-connect") + == 0) + return oidc_check_userid_openid_openidc(r, c); + + /* see if we've configured OAuth 2.0 access control for this request */ + if (apr_strnatcasecmp((const char *) ap_auth_type(r), "oauth20") == 0) + return oidc_oauth_check_userid(r, c); + + /* this is not for us but for some other handler */ + return DECLINED; +} + +#if MODULE_MAGIC_NUMBER_MAJOR >= 20100714 +/* + * generic Apache >=2.4 authorization hook for this module + * handles both OpenID Connect or OAuth 2.0 in the same way, based on the claims stored in the session + */ +authz_status oidc_authz_checker(request_rec *r, const char *require_args, const void *parsed_require_args) { + + /* get the set of claims from the request state (they've been set in the authentication part earlier */ + apr_json_value_t *attrs = (apr_json_value_t *)oidc_request_state_get(r, OIDC_CLAIMS_SESSION_KEY); + + /* dispatch to the >=2.4 specific authz routine */ + return oidc_authz_worker24(r, attrs, require_args); +} +#else +/* + * generic Apache <2.4 authorization hook for this module + * handles both OpenID Connect and OAuth 2.0 in the same way, based on the claims stored in the request context + */ +int oidc_auth_checker(request_rec *r) { + + /* get the set of claims from the request state (they've been set in the authentication part earlier) */ + apr_json_value_t *attrs = (apr_json_value_t *) oidc_request_state_get(r, + OIDC_CLAIMS_SESSION_KEY); + + /* get the Require statements */ + const apr_array_header_t * const reqs_arr = ap_requires(r); + + /* see if we have any */ + const require_line * const reqs = + reqs_arr ? (require_line *) reqs_arr->elts : NULL; + if (!reqs_arr) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "No require statements found, " + "so declining to perform authorization."); + return DECLINED; + } + + /* dispatch to the <2.4 specific authz routine */ + return oidc_authz_worker(r, attrs, reqs, reqs_arr->nelts); +} +#endif + +extern const command_rec oidc_config_cmds[]; + +module AP_MODULE_DECLARE_DATA auth_openidc_module = { + STANDARD20_MODULE_STUFF, + oidc_create_dir_config, + oidc_merge_dir_config, + oidc_create_server_config, + oidc_merge_server_config, + oidc_config_cmds, + oidc_register_hooks +}; diff --git a/src/mod_auth_openidc.h b/src/mod_auth_openidc.h new file mode 100644 index 00000000..0af7a457 --- /dev/null +++ b/src/mod_auth_openidc.h @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#ifndef MOD_AUTH_CONNECT_H_ +#define MOD_AUTH_CONNECT_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "apr_memcache.h" +#include "apr_shm.h" +#include "apr_global_mutex.h" + +#include "apr_json.h" + +#include "cache/cache.h" + +#ifndef OIDC_DEBUG +#define OIDC_DEBUG APLOG_DEBUG +#endif + +/* key for storing the claims in the session context */ +#define OIDC_CLAIMS_SESSION_KEY "claims" +/* key for storing the id_token in the session context */ +#define OIDC_IDTOKEN_SESSION_KEY "id_token" + +/* parameter name of the callback URL in the discovery response */ +#define OIDC_DISC_CB_PARAM "oidc_callback" +/* parameter name of the OP provider selection in the discovery response */ +#define OIDC_DISC_OP_PARAM "oidc_provider" +/* parameter name of the original URL in the discovery response */ +#define OIDC_DISC_RT_PARAM "oidc_return" + +/* value that indicates to use cache-file based session tracking */ +#define OIDC_SESSION_TYPE_22_CACHE_FILE 0 +/* value that indicates to use cookie based session tracking */ +#define OIDC_SESSION_TYPE_22_COOKIE 1 + +/* name of the cookie that binds the state in the authorization request/response to the browser */ +#define OIDCStateCookieName "mod_auth_openidc_state" +/* separator used to distinguish different values in the state cookie */ +#define OIDCStateCookieSep " " + +/* the (global) key for the mod_auth_openidc related state that is stored in the request userdata context */ +#define OIDC_USERDATA_KEY "mod_auth_openidc_state" + +/* use for plain GET in HTTP calls to endpoints */ +#define OIDC_HTTP_GET 0 +/* use for url-form-encoded HTTP POST calls to endpoints */ +#define OIDC_HTTP_POST_FORM 1 +/* use for JSON encoded POST calls to endpoints */ +#define OIDC_HTTP_POST_JSON 2 + +/* input filter hook name */ +#define OIDC_UTIL_HTTP_SENDSTRING "OIDC_UTIL_HTTP_SENDSTRING" + +typedef struct oidc_provider_t { + char *issuer; + char *authorization_endpoint_url; + char *token_endpoint_url; + char *token_endpoint_auth; + char *userinfo_endpoint_url; + char *jwks_uri; + char *client_id; + char *client_secret; + + // the next ones function as global default settings too + int ssl_validate_server; + char *client_name; + char *client_contact; + char *scope; + char *response_type; + int jwks_refresh_interval; + int idtoken_iat_slack; +} oidc_provider_t ; + +typedef struct oidc_oauth_t { + int ssl_validate_server; + char *client_id; + char *client_secret; + char *validate_endpoint_url; + char *validate_endpoint_auth; +} oidc_oauth_t; + +typedef struct oidc_cfg { + /* indicates whether this is a derived config, merged from a base one */ + unsigned int merged; + + /* the redirect URI as configured with the OpenID Connect OP's that we talk to */ + char *redirect_uri; + /* (optional) external OP discovery page */ + char *discover_url; + /* (optional) the signing algorithm the OP should use (used in dynamic client registration only) */ + char *id_token_alg; + + /* a pointer to the (single) provider that we connect to */ + /* NB: if metadata_dir is set, these settings will function as defaults for the metadata read from there) */ + oidc_provider_t provider; + /* a pointer to the oauth server settings */ + oidc_oauth_t oauth; + + /* directory that holds the provider & client metadata files */ + char *metadata_dir; + /* type of session management/storage */ + int session_type; + + /* pointer to cache functions */ + oidc_cache_t *cache; + void *cache_cfg; + /* cache_type = file: directory that holds the cache files (if not set, we'll try and use an OS defined one like "/tmp" */ + char *cache_file_dir; + /* cache_type = file: clean interval */ + int cache_file_clean_interval; + /* cache_type= memcache: list of memcache host/port servers to use */ + char *cache_memcache_servers; + /* cache_type = shm: size of the shared memory segment (cq. max number of cached entries) */ + int cache_shm_size_max; + + /* tell the module to strip any mod_auth_openidc related headers that already have been set by the user-agent, normally required for secure operation */ + int scrub_request_headers; + + int http_timeout_long; + int http_timeout_short; + int state_timeout; + + char *cookie_domain; + char *claim_delimiter; + char *claim_prefix; + + char *crypto_passphrase; + + EVP_CIPHER_CTX *encrypt_ctx; + EVP_CIPHER_CTX *decrypt_ctx; +} oidc_cfg; + +typedef struct oidc_dir_cfg { + char *cookie_path; + char *cookie; + char *authn_header; +} oidc_dir_cfg; + +int oidc_check_user_id(request_rec *r); +#if MODULE_MAGIC_NUMBER_MAJOR >= 20100714 +authz_status oidc_authz_checker(request_rec *r, const char *require_args, const void *parsed_require_args); +#else +int oidc_auth_checker(request_rec *r); +#endif +void oidc_request_state_set(request_rec *r, const char *key, const char *value); +const char*oidc_request_state_get(request_rec *r, const char *key); + +// oidc_oauth +int oidc_oauth_check_userid(request_rec *r, oidc_cfg *c); + +// oidc_proto.c +int oidc_proto_authorization_request(request_rec *r, struct oidc_provider_t *provider, const char *redirect_uri, const char *state, const char *original_url, const char *nonce); +apr_byte_t oidc_proto_is_basic_authorization_response(request_rec *r, oidc_cfg *cfg); +apr_byte_t oidc_proto_is_implicit_post(request_rec *r, oidc_cfg *cfg); +apr_byte_t oidc_proto_is_implicit_redirect(request_rec *r, oidc_cfg *cfg); +apr_byte_t oidc_proto_resolve_code(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, char *code, const char *nonce, char **user, apr_json_value_t **j_idtoken_payload, char **s_id_token, char **s_access_token, apr_time_t *expires); +apr_byte_t oidc_proto_resolve_userinfo(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, const char *access_token, const char **response, apr_json_value_t **claims); +apr_byte_t oidc_proto_account_based_discovery(request_rec *r, oidc_cfg *cfg, const char *acct, char **issuer); +apr_byte_t oidc_proto_parse_idtoken(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, const char *id_token, const char *nonce, char **user, apr_json_value_t **j_payload, char **s_payload, apr_time_t *expires); +int oidc_proto_javascript_implicit(request_rec *r, oidc_cfg *c); + +// oidc_authz.c +int oidc_authz_worker(request_rec *r, const apr_json_value_t *const claims, const require_line *const reqs, int nelts); +#if MODULE_MAGIC_NUMBER_MAJOR >= 20100714 +authz_status oidc_authz_worker24(request_rec *r, const apr_json_value_t * const claims, const char *require_line); +#endif + +// oidc_config.c +void *oidc_create_server_config(apr_pool_t *pool, server_rec *svr); +void *oidc_merge_server_config(apr_pool_t *pool, void *BASE, void *ADD); +void *oidc_create_dir_config(apr_pool_t *pool, char *path); +void *oidc_merge_dir_config(apr_pool_t *pool, void *BASE, void *ADD); +void oidc_register_hooks(apr_pool_t *pool); + +const char *oidc_set_flag_slot(cmd_parms *cmd, void *struct_ptr, int arg); +const char *oidc_set_int_slot(cmd_parms *cmd, void *struct_ptr, const char *arg); +const char *oidc_set_string_slot(cmd_parms *cmd, void *struct_ptr, const char *arg); +const char *oidc_set_https_slot(cmd_parms *cmd, void *struct_ptr, const char *arg); +const char *oidc_set_url_slot(cmd_parms *cmd, void *struct_ptr, const char *arg); +const char *oidc_set_endpoint_auth_slot(cmd_parms *cmd, void *struct_ptr, const char *arg); +const char *oidc_set_cookie_domain(cmd_parms *cmd, void *ptr, const char *value); +const char *oidc_set_dir_slot(cmd_parms *cmd, void *ptr, const char *arg); +const char *oidc_set_session_type(cmd_parms *cmd, void *ptr, const char *arg); +const char *oidc_set_cache_type(cmd_parms *cmd, void *ptr, const char *arg); +const char *oidc_set_response_type(cmd_parms *cmd, void *struct_ptr, const char *arg); +const char *oidc_set_id_token_alg(cmd_parms *cmd, void *struct_ptr, const char *arg); +const char *oidc_set_cache_shm_max(cmd_parms *cmd, void *ptr, const char *arg); + +char *oidc_get_cookie_path(request_rec *r); + +// oidc_util.c +int oidc_strnenvcmp(const char *a, const char *b, int len); +int oidc_base64url_encode(request_rec *r, char **dst, const char *src, int src_len); +int oidc_base64url_decode(request_rec *r, char **dst, const char *src, int padding); +void oidc_set_cookie(request_rec *r, const char *cookieName, const char *cookieValue); +char *oidc_get_cookie(request_rec *r, char *cookieName); +int oidc_encrypt_base64url_encode_string(request_rec *r, char **dst, const char *src); +int oidc_base64url_decode_decrypt_string(request_rec *r, char **dst, const char *src); +char *oidc_get_current_url(const request_rec *r, const oidc_cfg *c); +char *oidc_url_encode(const request_rec *r, const char *str, const char *charsToEncode); +char *oidc_normalize_header_name(const request_rec *r, const char *str); + +apr_byte_t oidc_util_http_call(request_rec *r, const char *url, int action, const apr_table_t *params, const char *basic_auth, const char *bearer_token, int ssl_validate_server, const char **response, int timeout); +apr_byte_t oidc_util_request_matches_url(request_rec *r, const char *url); +apr_byte_t oidc_util_request_has_parameter(request_rec *r, const char* param); +apr_byte_t oidc_util_get_request_parameter(request_rec *r, char *name, char **value); +apr_byte_t oidc_util_decode_json_and_check_error(request_rec *r, const char *str, apr_json_value_t **json); +int oidc_util_http_sendstring(request_rec *r, const char *html, int success_rvalue); +char *oidc_util_escape_string(const request_rec *r, const char *str); +char *oidc_util_unescape_string(const request_rec *r, const char *str); +apr_byte_t oidc_util_read_post(request_rec *r, apr_table_t *table); +apr_byte_t oidc_util_generate_random_base64url_encoded_value(request_rec *r, int randomLen, char **randomB64); +apr_byte_t oidc_util_file_read(request_rec *r, const char *path, char **result); +apr_byte_t oidc_util_issuer_match(const char *a, const char *b); +int oidc_util_html_send_error(request_rec *r, const char *error, const char *description, int status_code); +int oidc_base64url_decode_rsa_verify(request_rec *r, const char *alg, const char *signature, const char *message, const char *modulus, const char *exponent); +apr_byte_t oidc_util_json_array_has_value(request_rec *r, apr_json_value_t *haystack, const char *needle); + +// oidc_crypto.c +unsigned char *oidc_crypto_aes_encrypt(request_rec *r, oidc_cfg *cfg, unsigned char *plaintext, int *len); +unsigned char *oidc_crypto_aes_decrypt(request_rec *r, oidc_cfg *cfg, unsigned char *ciphertext, int *len); +char *oidc_crypto_jwt_alg2digest(const char *alg); +apr_byte_t oidc_crypto_rsa_verify(request_rec *r, const char *alg, unsigned char* sig, int sig_len, unsigned char* msg, int msg_len, unsigned char *mod, int mod_len, unsigned char *exp, int exp_len); +apr_byte_t oidc_crypto_hoidc_verify(request_rec *r, const char *alg, unsigned char* sig, int sig_len, unsigned char* msg, int msg_len, unsigned char *key, int key_len); + +// oidc_metadata.c +apr_byte_t oidc_metadata_list(request_rec *r, oidc_cfg *cfg, apr_array_header_t **arr); +apr_byte_t oidc_metadata_get(request_rec *r, oidc_cfg *cfg, const char *selected, oidc_provider_t **provider); +apr_byte_t oidc_metadata_jwks_get(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, apr_json_value_t **j_jwks, apr_byte_t *refresh); + +// oidc_session.c +#if MODULE_MAGIC_NUMBER_MAJOR >= 20081201 +// this stuff should make it easy to migrate to the post 2.3 mod_session infrastructure +#include "mod_session.h" +#else +typedef struct { + apr_pool_t *pool; /* pool to be used for this session */ + apr_uuid_t *uuid; /* anonymous uuid of this particular session */ + const char *remote_user; /* user who owns this particular session */ + apr_table_t *entries; /* key value pairs */ + const char *encoded; /* the encoded version of the key value pairs */ + apr_time_t expiry; /* if > 0, the time of expiry of this session */ + long maxage; /* if > 0, the maxage of the session, from + * which expiry is calculated */ + int dirty; /* dirty flag */ + int cached; /* true if this session was loaded from a + * cache of some kind */ + int written; /* true if this session has already been + * written */ +} session_rec; +#endif + +apr_status_t oidc_session_init(); +apr_status_t oidc_session_load(request_rec *r, session_rec **z); +apr_status_t oidc_session_get(request_rec *r, session_rec *z, const char *key, const char **value); +apr_status_t oidc_session_set(request_rec *r, session_rec *z, const char *key, const char *value); +apr_status_t oidc_session_save(request_rec *r, session_rec *z); +apr_status_t oidc_session_kill(request_rec *r, session_rec *z); + +#endif /* MOD_AUTH_CONNECT_H_ */ diff --git a/src/oauth.c b/src/oauth.c new file mode 100644 index 00000000..f62b4470 --- /dev/null +++ b/src/oauth.c @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include + +#include +#include +#include +#include + +#include "mod_auth_openidc.h" + +/* the grant type string that the Authorization server expects when validating access tokens */ +#define OIDC_OAUTH_VALIDATION_GRANT_TYPE "urn:pingidentity.com:oauth2:grant_type:validate_bearer" + +/* + * validates an access token against the validation endpoint of the Authorization server and gets a response back + */ +static int oidc_oauth_validate_access_token(request_rec *r, oidc_cfg *c, + const char *token, const char **response) { + + /* assemble parameters to call the token endpoint for validation */ + apr_table_t *params = apr_table_make(r->pool, 4); + apr_table_addn(params, "grant_type", OIDC_OAUTH_VALIDATION_GRANT_TYPE); + apr_table_addn(params, "token", token); + + /* see if we want to do basic auth or post-param-based auth */ + const char *basic_auth = NULL; + if ((apr_strnatcmp(c->oauth.validate_endpoint_auth, "client_secret_post")) + == 0) { + apr_table_addn(params, "client_id", c->oauth.client_id); + apr_table_addn(params, "client_secret", c->oauth.client_secret); + } else { + basic_auth = apr_psprintf(r->pool, "%s:%s", c->oauth.client_id, + c->oauth.client_secret); + } + + /* call the endpoint with the constructed parameter set and return the resulting response */ + return oidc_util_http_call(r, c->oauth.validate_endpoint_url, + OIDC_HTTP_POST_FORM, params, basic_auth, NULL, + c->oauth.ssl_validate_server, response, c->http_timeout_long); +} + +/* + * get the authorization header that should contain a bearer token + */ +static apr_byte_t oidc_oauth_get_bearer_token(request_rec *r, + const char **access_token) { + + /* get the authorization header */ + const char *auth_line; + auth_line = apr_table_get(r->headers_in, "Authorization"); + if (!auth_line) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_oauth_get_bearer_token: no authorization header found"); + return FALSE; + } + + /* look for the Bearer keyword */ + if (apr_strnatcasecmp(ap_getword(r->pool, &auth_line, ' '), "Bearer")) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_oauth_get_bearer_token: client used unsupported authentication scheme: %s", + r->uri); + return FALSE; + } + + /* skip any spaces after the Bearer keyword */ + while (apr_isspace(*auth_line)) { + auth_line++; + } + + /* copy the result in to the access_token */ + *access_token = apr_pstrdup(r->pool, auth_line); + + /* log some stuff */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_oauth_get_bearer_token: bearer token: %s", *access_token); + + return TRUE; +} + +static apr_byte_t oidc_oauth_resolve_access_token(request_rec *r, oidc_cfg *c, + const char *access_token, apr_json_value_t **token) { + + apr_json_value_t *result = NULL; + const char *json = NULL; + + /* see if we've got the claims for this access_token cached already */ + c->cache->get(r, access_token, &json); + + if (json == NULL) { + + /* not cached, go out and validate the access_token against the Authorization server and get the JSON claims back */ + if (oidc_oauth_validate_access_token(r, c, access_token, &json) == FALSE) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_oauth_resolve_access_token: could not get a validation response from the Authorization server"); + return FALSE; + } + + /* decode and see if it is not an error response somehow */ + if (oidc_util_decode_json_and_check_error(r, json, &result) == FALSE) + return FALSE; + + /* get and check the expiry timestamp */ + apr_json_value_t *expires_in = apr_hash_get(result->value.object, + "expires_in", APR_HASH_KEY_STRING); + if ((expires_in == NULL) || (expires_in->type != APR_JSON_LONG)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_oauth_resolve_access_token: response JSON object did not contain an \"expires_in\" number"); + return FALSE; + } + if (expires_in->value.lnumber <= 0) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_oauth_resolve_access_token: \"expires_in\" number <= 0 (%ld); token already expired...", + expires_in->value.lnumber); + return FALSE; + } + + /* set it in the cache so subsequent request don't need to validate the access_token and get the claims anymore */ + c->cache->set(r, access_token, json, + apr_time_now() + apr_time_from_sec(expires_in->value.lnumber)); + + } else { + + /* we got the claims for this access_token in our cache, decode it in to a JSON structure */ + if (apr_json_decode(&result, json, strlen(json), r->pool) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_oauth_resolve_access_token: cached JSON was corrupted"); + return FALSE; + } + /* the NULL and APR_JSON_OBJECT checks really are superfluous here */ + } + + /* return the access_token JSON object */ + *token = apr_hash_get(result->value.object, "access_token", + APR_HASH_KEY_STRING); + if ((*token == NULL) || ((*token)->type != APR_JSON_OBJECT)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_oauth_resolve_access_token: response JSON object did not contain an access_token object"); + return FALSE; + } + + return TRUE; +} + +int oidc_oauth_check_userid(request_rec *r, oidc_cfg *c) { + + /* check if this is a sub-request or an initial request */ + if (!ap_is_initial_req(r)) { + + if (r->main != NULL) + r->user = r->main->user; + else if (r->prev != NULL) + r->user = r->prev->user; + + if (r->user != NULL) { + + /* this is a sub-request and we have a session */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_oauth_check_userid: recycling user '%s' from initial request for sub-request", + r->user); + + return OK; + } + } + + /* we don't have a session yet */ + + /* get the bearer access token from the Authorization header */ + const char *access_token = NULL; + if (oidc_oauth_get_bearer_token(r, &access_token) == FALSE) + return HTTP_UNAUTHORIZED; + + /* validate the obtained access token against the OAuth AS validation endpoint */ + apr_json_value_t *token = NULL; + if (oidc_oauth_resolve_access_token(r, c, access_token, &token) == FALSE) + return HTTP_UNAUTHORIZED; + + /* store the parsed token (cq. the claims from the response) in the request state so it can be accessed by the authz routines */ + oidc_request_state_set(r, OIDC_CLAIMS_SESSION_KEY, (const char *) token); + + // TODO: user attribute header settings & scrubbing ? + + /* get the username from the response to use as the REMOTE_USER key */ + apr_json_value_t *username = apr_hash_get(token->value.object, "Username", + APR_HASH_KEY_STRING); + if ((username == NULL) || (username->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_oauth_check_userid: response JSON object did not contain a Username string"); + } else { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_oauth_check_userid: returned username: %s", + username->value.string.p); + r->user = apr_pstrdup(r->pool, username->value.string.p); + } + + return OK; +} diff --git a/src/proto.c b/src/proto.c new file mode 100644 index 00000000..da97e12a --- /dev/null +++ b/src/proto.c @@ -0,0 +1,938 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include +#include +#include + +#include "mod_auth_openidc.h" + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +/* + * send an OpenID Connect authorization request to the specified provider + */ +int oidc_proto_authorization_request(request_rec *r, + struct oidc_provider_t *provider, const char *redirect_uri, + const char *state, const char *original_url, const char *nonce) { + + /* log some stuff */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_authorization_request: entering (issuer=%s, redirect_uri=%s, original_url=%s, state=%s, nonce=%s)", + provider->issuer, redirect_uri, original_url, state, nonce); + + /* assemble the full URL as the authorization request to the OP where we want to redirect to */ + char *destination = + apr_psprintf(r->pool, + "%s%sresponse_type=%s&scope=%s&client_id=%s&state=%s&redirect_uri=%s", + provider->authorization_endpoint_url, + (strchr(provider->authorization_endpoint_url, '?') != NULL ? + "&" : "?"), oidc_util_escape_string(r, provider->response_type), + oidc_util_escape_string(r, provider->scope), + oidc_util_escape_string(r, provider->client_id), + oidc_util_escape_string(r, state), + oidc_util_escape_string(r, redirect_uri)); + + /* + * see if the chosen flow requires a nonce parameter + * + * TODO: I'd like to include the nonce in the code flow as well but Google does not allow me to do that: + * Error: invalid_request: Parameter not allowed for this message type: nonce + */ + if ( (strstr(provider->response_type, "id_token") != NULL) || (strcmp(provider->response_type, "token") == 0) ) { + destination = apr_psprintf(r->pool, "%s&nonce=%s", destination, oidc_util_escape_string(r, nonce)); + //destination = apr_psprintf(r->pool, "%s&response_mode=fragment", destination); + } + + /* add the redirect location header */ + apr_table_add(r->headers_out, "Location", destination); + + /* some more logging */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_authorization_request: adding outgoing header: Location: %s", + destination); + + /* and tell Apache to return an HTTP Redirect (302) message */ + return HTTP_MOVED_TEMPORARILY; +} + +/* + * indicate whether the incoming HTTP request is an OpenID Connect Authorization Response from a Basic Client flow, syntax-wise + */ +apr_byte_t oidc_proto_is_basic_authorization_response(request_rec *r, oidc_cfg *cfg) { + + /* see if this is a call to the configured redirect_uri and the "code" and "state" parameters are present */ + return ((oidc_util_request_matches_url(r, cfg->redirect_uri) == TRUE) + && oidc_util_request_has_parameter(r, "code") + && oidc_util_request_has_parameter(r, "state")); +} + +/* + * indicate whether the incoming HTTP request is an OpenID Connect Authorization Response from an Implicit Client flow, syntax-wise + */ +apr_byte_t oidc_proto_is_implicit_post(request_rec *r, oidc_cfg *cfg) { + + /* see if this is a call to the configured redirect_uri and it is a POST */ + return ((oidc_util_request_matches_url(r, cfg->redirect_uri) == TRUE) + && (r->method_number == M_POST)); +} + +/* + * indicate whether the incoming HTTP request is an OpenID Connect Authorization Response from an Implicit Client flow using the query parameter response type, syntax-wise + */ +apr_byte_t oidc_proto_is_implicit_redirect(request_rec *r, oidc_cfg *cfg) { + + /* see if this is a call to the configured redirect_uri and it is a POST */ + return ((oidc_util_request_matches_url(r, cfg->redirect_uri) == TRUE) + && (r->method_number == M_GET) + && oidc_util_request_has_parameter(r, "state") + && oidc_util_request_has_parameter(r, "id_token")); +} + +/* + * check whether the provided JSON payload (in the j_payload parameter) is a valid id_token for the specified "provider" + */ +static apr_byte_t oidc_proto_is_valid_idtoken(request_rec *r, + oidc_provider_t *provider, apr_json_value_t *j_payload, const char *nonce, + apr_time_t *expires) { + + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, + &auth_openidc_module); + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_is_valid_idtoken: entering (looking for nonce=%s)", nonce); + + /* if a nonce is not passed, we're doing a ("code") flow where the nonce is optional */ + if (nonce != NULL) { + + /* see if we've this nonce cached already */ + const char *replay = NULL; + cfg->cache->get(r, nonce, &replay); + if (replay != NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: nonce was found in cache already; replay attack!?"); + return FALSE; + } + + apr_json_value_t *j_nonce = apr_hash_get(j_payload->value.object, "nonce", + APR_HASH_KEY_STRING); + if ((j_nonce == NULL) || (j_nonce->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: response JSON object did not contain a \"nonce\" string"); + return FALSE; + } + if (strcmp(nonce, j_nonce->value.string.p) != 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: the nonce value in the id_token did not match the one stored in the browser session"); + return FALSE; + } + } + + /* get the "issuer" value from the JSON payload */ + apr_json_value_t *iss = apr_hash_get(j_payload->value.object, "iss", + APR_HASH_KEY_STRING); + if ((iss == NULL) || (iss->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: response JSON object did not contain an \"iss\" string"); + return FALSE; + } + + /* check if the issuer matches the requested value */ + if (oidc_util_issuer_match(provider->issuer, iss->value.string.p) == FALSE) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: configured issuer (%s) does not match received \"iss\" value in id_token (%s)", + provider->issuer, iss->value.string.p); + return FALSE; + } + + /* get the "exp" value from the JSON payload */ + apr_json_value_t *exp = apr_hash_get(j_payload->value.object, "exp", + APR_HASH_KEY_STRING); + if ((exp == NULL) || (exp->type != APR_JSON_LONG)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: response JSON object did not contain an \"exp\" number value"); + return FALSE; + } + + /* check if this id_token has already expired */ + if (apr_time_sec(apr_time_now()) > exp->value.lnumber) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: id_token expired"); + return FALSE; + } + + /* return the "exp" value in the "expires" return parameter */ + *expires = apr_time_from_sec(exp->value.lnumber); + + /* get the "iat" value from the JSON payload */ + apr_json_value_t *iat = apr_hash_get(j_payload->value.object, "iat", + APR_HASH_KEY_STRING); + if ((iat == NULL) || (iat->type != APR_JSON_LONG)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: response JSON object did not contain an \"iat\" number value"); + return FALSE; + } + + /* check if this id_token has been issued just now +- slack (default 10 minutes) */ + if ((apr_time_sec(apr_time_now()) - provider->idtoken_iat_slack) > iat->value.lnumber) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: token was issued more than %d seconds ago", provider->idtoken_iat_slack); + return FALSE; + } + if ((apr_time_sec(apr_time_now()) + provider->idtoken_iat_slack) < iat->value.lnumber) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: token was issued more than %d seconds in the future", provider->idtoken_iat_slack); + return FALSE; + } + + if (nonce != NULL) { + /* cache the nonce for the window time of the token for replay prevention plus 10 seconds for safety */ + cfg->cache->set(r, nonce, nonce, apr_time_from_sec(provider->idtoken_iat_slack * 2 + 10)); + } + + /* get the "azp" value from the JSON payload, which may be NULL */ + apr_json_value_t *azp = apr_hash_get(j_payload->value.object, "azp", + APR_HASH_KEY_STRING); + if ((azp != NULL) && (azp->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: id_token JSON payload contained an \"azp\" value, but it was not a string"); + return FALSE; + } + + /* + * This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. + * It MAY be included even when the authorized party is the same as the sole audience. + */ + if ((azp != NULL) + && (strcmp(azp->value.string.p, provider->client_id) != 0)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "\"azp\" claim (%s) is not equal to configured client_id (%s)", + azp->value.string.p, provider->client_id); + return FALSE; + } + + /* get the "aud" value from the JSON payload */ + apr_json_value_t *aud = apr_hash_get(j_payload->value.object, "aud", + APR_HASH_KEY_STRING); + + if (aud != NULL) { + + /* check if it is a single-value */ + if (aud->type == APR_JSON_STRING) { + + /* a single-valued audience must be equal to our client_id */ + if (strcmp(aud->value.string.p, provider->client_id) != 0) { + + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: configured client_id (%s) did not match the JSON \"aud\" entry (%s)", + provider->client_id, aud->value.string.p); + return FALSE; + } + + /* check if this is a multi-valued audience */ + } else if (aud->type == APR_JSON_ARRAY) { + + if ((aud->value.array->nelts > 1) && (azp == NULL)) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_is_valid_idtoken: \"aud\" is an array with more than 1 element, but \"azp\" claim is not present (a SHOULD in the spec...)"); + } + + /* loop over the audience values */ + int i; + for (i = 0; i < aud->value.array->nelts; i++) { + + apr_json_value_t *elem = + APR_ARRAY_IDX(aud->value.array, i, apr_json_value_t *); + + /* check if it is a string, warn otherwise */ + if (elem->type != APR_JSON_STRING) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_is_valid_idtoken: unhandled in-array JSON object type [%d]", + elem->type); + continue; + } + + /* we're looking for a value in the list that matches our client id */ + if (strcmp(elem->value.string.p, provider->client_id) == 0) { + break; + } + } + + /* check if we've found a match or not */ + if (i == aud->value.array->nelts) { + + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: configured client_id (%s) could not be found in the JSON \"aud\" array object", + provider->client_id); + return FALSE; + } + + } else { + + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: response JSON \"aud\" object is not a string nor an array"); + return FALSE; + } + + } else { + + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken: response JSON object did not contain an \"aud\" element"); + return FALSE; + } + + return TRUE; +} + +/* + * check whether the provider string is a valid id_token for the specified "provider" + */ +static apr_byte_t oidc_proto_is_valid_idtoken_payload(request_rec *r, + oidc_provider_t *provider, const char *s_idtoken_payload, const char *nonce, + apr_json_value_t **result, apr_time_t *expires) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_is_valid_idtoken_payload: entering (%s)", s_idtoken_payload); + + /* decode the string in to a JSON structure */ + if (apr_json_decode(result, s_idtoken_payload, strlen(s_idtoken_payload), + r->pool) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken_payload: could not decode id_token payload string in to a JSON structure"); + return FALSE; + } + + /* a convenient helper pointer */ + apr_json_value_t *j_payload = *result; + + /* check that we've actually got a JSON object back */ + if ((j_payload == NULL) || (j_payload->type != APR_JSON_OBJECT)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_is_valid_idtoken_payload: payload from id_token did not contain a JSON object"); + return FALSE; + } + + /* now check if the JSON is a valid id_token */ + return oidc_proto_is_valid_idtoken(r, provider, j_payload, nonce, expires); +} + +/* + * check whether the provided string is a valid id_token header + */ +static apr_byte_t oidc_proto_parse_idtoken_header(request_rec *r, + const char *s_header, apr_json_value_t **result) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_parse_idtoken_header: entering (%s)", s_header); + + /* decode the string in to a JSON structure */ + if (apr_json_decode(result, s_header, strlen(s_header), + r->pool) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_parse_idtoken_header: could not decode header from id_token successfully"); + return FALSE; + } + + /* a convenient helper pointer */ + apr_json_value_t *j_header = *result; + + /* check that we've actually got a JSON object back */ + if ((j_header == NULL) || (j_header->type != APR_JSON_OBJECT)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_parse_idtoken_header: header from id_token did not contain a JSON object"); + return FALSE; + } + + apr_json_value_t *algorithm = apr_hash_get(j_header->value.object, "alg", + APR_HASH_KEY_STRING); + if ((algorithm == NULL) || (algorithm->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_parse_idtoken_header: header JSON object did not contain a \"alg\" string"); + return FALSE; + } + + if (oidc_crypto_jwt_alg2digest(algorithm->value.string.p) == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_parse_idtoken_header: unsupported signing algorithm: %s", algorithm->value.string.p); + return FALSE; + } + + return TRUE; +} + +/* + * get the key from the JWKs that corresponds with the key specified in the header + */ +static apr_json_value_t *oidc_proto_get_key_from_jwks(request_rec *r, apr_json_value_t *j_header, apr_json_value_t *j_jwks) { + + const char *s_kid_match = NULL; + + apr_json_value_t *kid = apr_hash_get(j_header->value.object, "kid", APR_HASH_KEY_STRING); + if (kid != NULL) { + if (kid->type != APR_JSON_STRING) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_get_key_from_jwks: \"kid\" is not a string"); + return NULL;; + } + s_kid_match = kid->value.string.p; + } + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_proto_get_key_from_jwks: search for kid \"%s\"", s_kid_match); + + apr_json_value_t *keys = apr_hash_get(j_jwks->value.object, "keys", APR_HASH_KEY_STRING); + + int i; + for (i = 0; i < keys->value.array->nelts; i++) { + + apr_json_value_t *elem = APR_ARRAY_IDX(keys->value.array, i, apr_json_value_t *); + if (elem->type != APR_JSON_OBJECT) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_get_key_from_jwks: \"keys\" array element is not a JSON object, skipping"); + continue; + } + apr_json_value_t *kty = apr_hash_get(elem->value.object, "kty", APR_HASH_KEY_STRING); + if (strcmp(kty->value.string.p, "RSA") != 0) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_get_key_from_jwks: \"keys\" array element is not an RSA key type (%s), skipping", kty->value.string.p); + continue; + } + if (s_kid_match == NULL) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_proto_get_key_from_jwks: no kid to match, return first key found"); + return elem; + } + apr_json_value_t *ekid = apr_hash_get(elem->value.object, "kid", APR_HASH_KEY_STRING); + if (ekid == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_get_key_from_jwks: \"keys\" array element does not have a \"kid\" entry, skipping"); + continue; + } + if (strcmp(s_kid_match, ekid->value.string.p) == 0) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_proto_get_key_from_jwks: found matching kid: \"%s\"", s_kid_match); + return elem; + } + } + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_proto_get_key_from_jwks: return NULL"); + + return NULL; +} + +/* + * get the key from the (possibly cached) set of JWKs on the jwk_uri that corresponds with the key specified in the header + */ +static apr_json_value_t * oidc_proto_get_key_from_jwk_uri(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, apr_json_value_t *j_header, apr_byte_t *refresh) { + apr_json_value_t *j_jwks = NULL; + apr_json_value_t *key = NULL; + + /* get the set of JSON Web Keys for this provider (possibly by downloading them from the specified provider->jwk_uri) */ + oidc_metadata_jwks_get(r, cfg, provider, &j_jwks, refresh); + if (j_jwks == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_get_key_from_jwk_uri: could not resolve JSON Web Keys"); + return NULL; + } + + /* get the key corresponding to the kid from the header, referencing the key that was used to sign this message */ + key = oidc_proto_get_key_from_jwks(r, j_header, j_jwks); + + /* see what we've got back */ + if ( (key == NULL) && (refresh == FALSE) ) { + + /* we did not get a key, but we have not refreshed the JWKs from the jwks_uri yet */ + + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_get_key_from_jwk_uri: could not find a key in the cached JSON Web Keys, doing a forced refresh"); + + /* get the set of JSON Web Keys for this provider forcing a fresh download from the specified provider->jwk_uri) */ + *refresh = TRUE; + oidc_metadata_jwks_get(r, cfg, provider, &j_jwks, refresh); + if (j_jwks == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_get_key_from_jwk_uri: could not refresh JSON Web Keys"); + return NULL; + } + + key = oidc_proto_get_key_from_jwks(r, j_header, j_jwks); + + } + + return key; +} + +static apr_byte_t oidc_proto_idtoken_verify_hmac(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, apr_json_value_t *j_header, const char *signature, const char *message) { + + unsigned char *key = (unsigned char *)provider->client_secret; + int key_len = strlen(provider->client_secret); + + unsigned char *sig = NULL; + int sig_len = oidc_base64url_decode(r, (char **)&sig, signature, 1); + + apr_json_value_t *alg = apr_hash_get(j_header->value.object, "alg", + APR_HASH_KEY_STRING); + + return oidc_crypto_hoidc_verify(r, alg->value.string.p, sig, sig_len, (unsigned char *)message, strlen(message), key, key_len); +} + +/* + * verify the signature on an id_token + */ +static apr_byte_t oidc_proto_idtoken_verify_signature(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, apr_json_value_t *j_header, const char *signature, const char *message, apr_byte_t *refresh) { + + /* get the key from the JWKs that corresponds with the key specified in the header */ + apr_json_value_t *key = oidc_proto_get_key_from_jwk_uri(r, cfg, provider, j_header, refresh); + if (key == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_idtoken_verify_signature: could not find a key in the JSON Web Keys"); + if (*refresh == FALSE) { + *refresh = TRUE; + return oidc_proto_idtoken_verify_signature(r, cfg, provider, j_header, signature, message, refresh); + } + return FALSE; + } + + /* get the modulus */ + apr_json_value_t *modulus = apr_hash_get(key->value.object, "n", APR_HASH_KEY_STRING); + if ((modulus == NULL) || (modulus->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_idtoken_verify_signature: response JSON object did not contain a \"n\" string"); + return FALSE; + } + + /* get the exponent */ + apr_json_value_t *exponent = apr_hash_get(key->value.object, "e", APR_HASH_KEY_STRING); + if ((exponent == NULL) || (exponent->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_idtoken_verify_signature: response JSON object did not contain a \"e\" string"); + return FALSE; + } + + /* do the actual signature verification */ + apr_json_value_t *algorithm = apr_hash_get(j_header->value.object, "alg", + APR_HASH_KEY_STRING); + + if (oidc_base64url_decode_rsa_verify(r, algorithm->value.string.p, signature, message, modulus->value.string.p, exponent->value.string.p) != TRUE) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_idtoken_verify_signature: signature verification on id_token failed"); + + if (*refresh == FALSE) { + *refresh = TRUE; + return oidc_proto_idtoken_verify_signature(r, cfg, provider, j_header, signature, message, refresh); + } + return FALSE; + } + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_proto_idtoken_verify_signature: signature with algorithm \"%s\" verified OK!", algorithm->value.string.p); + + /* if we've made it this far, all is OK */ + return TRUE; +} + +/* + * check whether the provided string is a valid id_token and return its parsed contents + */ +apr_byte_t oidc_proto_parse_idtoken(request_rec *r, oidc_cfg *cfg, + oidc_provider_t *provider, const char *id_token, const char *nonce, char **user, + apr_json_value_t **j_payload, char **s_payload, apr_time_t *expires) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_parse_idtoken: entering"); + + /* find the header */ + const char *s = apr_pstrdup(r->pool, id_token); + char *p = strchr(s, '.'); + if (p == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_parse_id_token: could not find first \".\" in id_token"); + return FALSE; + } + *p = '\0'; + + /* add to the message part that is signed */ + char *header = apr_pstrdup(r->pool, s); + + /* parse the header (base64decode, json_decode) and validate it */ + char *s_header = NULL; + oidc_base64url_decode(r, &s_header, s, 1); + apr_json_value_t *j_header = NULL; + if (oidc_proto_parse_idtoken_header(r, s_header, &j_header) != TRUE) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_parse_id_token: header parsing failure"); + return FALSE; + } + + /* find the payload */ + s = ++p; + p = strchr(s, '.'); + if (p == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_parse_id_token: could not find second \".\" in id_token"); + return FALSE; + } + *p = '\0'; + + char *payload = apr_pstrdup(r->pool, s); + + s = ++p; + + char *signature = apr_pstrdup(r->pool, s); + + // verify signature unless we did 'code' flow and the algorithm is NONE + // TODO: should improve "detection": in principle nonce can be used in "code" flow too +// apr_json_value_t *algorithm = apr_hash_get(j_header->value.object, "alg", APR_HASH_KEY_STRING); +// if ((strcmp(algorithm->value.string.p, "NONE") != 0) || (nonce != NULL)) { +// /* verify the signature on the id_token */ +// apr_byte_t refresh = FALSE; +// if (oidc_proto_idtoken_verify_signature(r, cfg, provider, j_header, signature, apr_pstrcat(r->pool, header, ".", payload, NULL), &refresh) == FALSE) return FALSE; +// } + + apr_json_value_t *algorithm = apr_hash_get(j_header->value.object, "alg", APR_HASH_KEY_STRING); + if (strncmp(algorithm->value.string.p, "HS", 2) == 0) { + /* verify the HOIDC signature on the id_token */ + if (oidc_proto_idtoken_verify_hmac(r, cfg, provider, j_header, signature, apr_pstrcat(r->pool, header, ".", payload, NULL)) == FALSE) return FALSE; + } else { + /* verify the RSA signature on the id_token */ + apr_byte_t refresh = FALSE; + if (oidc_proto_idtoken_verify_signature(r, cfg, provider, j_header, signature, apr_pstrcat(r->pool, header, ".", payload, NULL), &refresh) == FALSE) return FALSE; + } + + /* parse the payload */ + oidc_base64url_decode(r, s_payload, payload, 1); + + /* this is where the meat is */ + if (oidc_proto_is_valid_idtoken_payload(r, provider, *s_payload, nonce, j_payload, + expires) == FALSE) + return FALSE; + + /* extract and return the user name claim ("sub" or something similar) */ + apr_json_value_t *username = apr_hash_get((*j_payload)->value.object, "sub", + APR_HASH_KEY_STRING); + if ((username == NULL) || (username->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_parse_id_token: response JSON object did not contain a \"sub\" string, falback to non-spec compliant (MS) \"unique_name\""); + + username = apr_hash_get((*j_payload)->value.object, "unique_name", + APR_HASH_KEY_STRING); + + if ((username == NULL) || (username->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_parse_id_token: response JSON object did not contain a \"unique_name\" string either, second falback to non-spec compliant \"email\""); + + username = apr_hash_get((*j_payload)->value.object, "email", + APR_HASH_KEY_STRING); + + if ((username == NULL) || (username->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_proto_parse_id_token: response JSON object did not contain an \"email\" string either, now fail..."); + + return FALSE; + } + } + } + + /* set the unique username in the session (r->user) */ + *user = apr_pstrdup(r->pool, username->value.string.p); + + /* log our results */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_parse_idtoken: valid id_token for user \"%s\" (expires in %" APR_TIME_T_FMT " seconds)", + *user, *expires - apr_time_sec(apr_time_now())); + + /* since we've made it so far, we may as well say it is a valid id_token */ + return TRUE; +} + +/* + * resolves the code received from the OP in to an access_token and id_token and returns the parsed contents + */ +apr_byte_t oidc_proto_resolve_code(request_rec *r, oidc_cfg *cfg, + oidc_provider_t *provider, char *code, const char *nonce, char **user, + apr_json_value_t **j_idtoken_payload, char **s_id_token, + char **s_access_token, apr_time_t *expires) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_resolve_code: entering"); + const char *response = NULL; + + /* assemble the parameters for a call to the token endpoint */ + apr_table_t *params = apr_table_make(r->pool, 5); + apr_table_addn(params, "grant_type", "authorization_code"); + apr_table_addn(params, "code", code); + apr_table_addn(params, "redirect_uri", cfg->redirect_uri); + + /* see if we need to do basic auth or auth-through-post-params (both applied through the HTTP POST method though) */ + const char *basic_auth = NULL; + if ((apr_strnatcmp(provider->token_endpoint_auth, "client_secret_basic")) + == 0) { + basic_auth = apr_psprintf(r->pool, "%s:%s", provider->client_id, + provider->client_secret); + } else { + apr_table_addn(params, "client_id", provider->client_id); + apr_table_addn(params, "client_secret", provider->client_secret); + } +/* + if (strcmp(provider->issuer, "https://sts.windows.net/b4ea3de6-839e-4ad1-ae78-c78e5c0cdc06/") == 0) { + apr_table_addn(params, "resource", "https://graph.windows.net"); + } +*/ + /* resolve the code against the token endpoint */ + if (oidc_util_http_call(r, provider->token_endpoint_url, + OIDC_HTTP_POST_FORM, params, basic_auth, NULL, + provider->ssl_validate_server, &response, + cfg->http_timeout_long) == FALSE) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_resolve_code: could not successfully resolve the \"code\" (%s) against the token endpoint (%s)", + code, provider->token_endpoint_url); + return FALSE; + } + + /* check for errors, the response itself will have been logged already */ + apr_json_value_t *result = NULL; + if (oidc_util_decode_json_and_check_error(r, response, &result) == FALSE) + return FALSE; + + /* get the access_token from the parsed response */ + apr_json_value_t *access_token = apr_hash_get(result->value.object, + "access_token", APR_HASH_KEY_STRING); + if ((access_token == NULL) || (access_token->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_resolve_code: response JSON object did not contain an access_token string"); + return FALSE; + } + + /* log and set the obtained acces_token */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_resolve_code: returned access_token: %s", + access_token->value.string.p); + *s_access_token = apr_pstrdup(r->pool, access_token->value.string.p); + + /* the provider must the token type */ + apr_json_value_t *token_type = apr_hash_get(result->value.object, + "token_type", APR_HASH_KEY_STRING); + if ((token_type == NULL) || (token_type->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_resolve_code: response JSON object did not contain a token_type string"); + return FALSE; + } + + /* we got the type, we only support bearer/Bearer, check that */ + if ((apr_strnatcasecmp(token_type->value.string.p, "Bearer") != 0) + && (provider->userinfo_endpoint_url != NULL)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_resolve_code: token_type is \"%s\" and UserInfo endpoint is set: can only deal with Bearer authentication against the UserInfo endpoint!", + token_type->value.string.p); + return FALSE; + } + + /* get the id_token from the response */ + apr_json_value_t *id_token = apr_hash_get(result->value.object, "id_token", + APR_HASH_KEY_STRING); + if ((id_token == NULL) || (id_token->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_resolve_code: response JSON object did not contain an id_token string"); + return FALSE; + } + + /* log and set the obtained id_token */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_resolve_code: returned id_token: %s", + id_token->value.string.p); + *s_id_token = apr_pstrdup(r->pool, id_token->value.string.p); + + char *s_payload = NULL; + + /* parse and validate the obtained id_token and return success/failure of that */ + return oidc_proto_parse_idtoken(r, cfg, provider, id_token->value.string.p, nonce, user, + j_idtoken_payload, &s_payload, expires); +} + +/* + * get claims from the OP UserInfo endpoint using the provided access_token + */ +apr_byte_t oidc_proto_resolve_userinfo(request_rec *r, oidc_cfg *cfg, + oidc_provider_t *provider, const char *access_token, + const char **response, apr_json_value_t **claims) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_resolve_userinfo: entering, endpoint=%s, access_token=%s", + provider->userinfo_endpoint_url, access_token); + + /* only do this if an actual endpoint was set */ + if (provider->userinfo_endpoint_url == NULL) + return FALSE; + + /* get the JSON response */ + if (oidc_util_http_call(r, provider->userinfo_endpoint_url, OIDC_HTTP_GET, + NULL, NULL, access_token, provider->ssl_validate_server, response, + cfg->http_timeout_long) == FALSE) + return FALSE; + + /* decode and check for an "error" response */ + return oidc_util_decode_json_and_check_error(r, *response, claims); +} + +/* + * based on an account name, perform OpenID Connect Provider Issuer Discovery to find out the issuer and obtain and store its metadata + */ +apr_byte_t oidc_proto_account_based_discovery(request_rec *r, oidc_cfg *cfg, + const char *acct, char **issuer) { + + // TODO: maybe show intermediate/progress screen "discovering..." + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_account_based_discovery: entering, acct=%s", acct); + + const char *resource = apr_psprintf(r->pool, "acct:%s", acct); + const char *domain = strrchr(acct, '@'); + if (domain == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_account_based_discovery: invalid account name"); + return FALSE; + } + domain++; + const char *url = apr_psprintf(r->pool, "https://%s/.well-known/webfinger", + domain); + + apr_table_t *params = apr_table_make(r->pool, 1); + apr_table_addn(params, "resource", resource); + apr_table_addn(params, "rel", "http://openid.net/specs/connect/1.0/issuer"); + + const char *response = NULL; + if (oidc_util_http_call(r, url, OIDC_HTTP_GET, params, NULL, NULL, + cfg->provider.ssl_validate_server, &response, + cfg->http_timeout_short) == FALSE) { + /* errors will have been logged by now */ + return FALSE; + } + + /* decode and see if it is not an error response somehow */ + apr_json_value_t *j_response = NULL; + if (oidc_util_decode_json_and_check_error(r, response, &j_response) == FALSE) + return FALSE; + + /* get the links parameter */ + apr_json_value_t *j_links = apr_hash_get(j_response->value.object, "links", + APR_HASH_KEY_STRING); + if ((j_links == NULL) || (j_links->type != APR_JSON_ARRAY)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_account_based_discovery: response JSON object did not contain a \"links\" array"); + return FALSE; + } + + /* get the one-and-only object in the "links" array */ + apr_json_value_t *j_object = + ((apr_json_value_t**) j_links->value.array->elts)[0]; + if ((j_object == NULL) || (j_object->type != APR_JSON_OBJECT)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_account_based_discovery: response JSON object did not contain a JSON object as the first element in the \"links\" array"); + return FALSE; + } + + /* get the href from that object, which is the issuer value */ + apr_json_value_t *j_href = apr_hash_get(j_object->value.object, "href", + APR_HASH_KEY_STRING); + if ((j_href == NULL) || (j_href->type != APR_JSON_STRING)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_proto_account_based_discovery: response JSON object did not contain a \"href\" element in the first \"links\" array object"); + return FALSE; + } + + *issuer = (char *) j_href->value.string.p; + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_proto_account_based_discovery: returning issuer \"%s\" for account \"%s\" after doing succesful webfinger-based discovery", + *issuer, acct); + + return TRUE; +} + +int oidc_proto_javascript_implicit(request_rec *r, oidc_cfg *c) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_proto_javascript_implicit: entering"); + +// char *java_script = NULL; +// if (oidc_util_file_read(r, "/Users/hzandbelt/eclipse-workspace/mod_auth_openidc/src/implicit_post.html", &java_script) == FALSE) return HTTP_INTERNAL_SERVER_ERROR; + + const char *java_script = + "\n" + "\n" + " \n" + " \n" + " \n" + " Submitting...\n" + " \n" + " \n" + "

Submitting...

\n" + "
\n" + " \n" + "\n"; + + //return oidc_util_http_sendstring(r, apr_psprintf(r->pool, java_script, c->redirect_uri), OK); + //return oidc_util_http_sendstring(r, apr_psprintf(r->pool, java_script, c->redirect_uri), HTTP_MOVED_TEMPORARILY); + return oidc_util_http_sendstring(r, java_script, HTTP_UNAUTHORIZED); +} + diff --git a/src/session.c b/src/session.c new file mode 100644 index 00000000..dcbdcbb2 --- /dev/null +++ b/src/session.c @@ -0,0 +1,556 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include + +#include +#include +#include +#include + +#include "mod_auth_openidc.h" + +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +/* the name of the remote-user attribute in the session */ +#define OIDC_SESSION_REMOTE_USER_KEY "remote-user" +/* the name of the session expiry attribute in the session */ +#define OIDC_SESSION_EXPIRY_KEY "mac-expiry" +/* the name of the uuid attribute in the session */ +#define OIDC_SESSION_UUID_KEY "mac-uuid" + +static apr_status_t (*ap_session_load_fn)(request_rec *r, session_rec **z) = NULL; +static apr_status_t (*ap_session_get_fn)(request_rec *r, session_rec *z, const char *key, const char **value) = NULL; +static apr_status_t (*ap_session_set_fn)(request_rec *r, session_rec *z, const char *key, const char *value) = NULL; +static apr_status_t (*ap_session_save_fn)(request_rec *r, session_rec *z) = NULL; + +apr_status_t oidc_session_load(request_rec *r, session_rec **zz) { + apr_status_t rc = ap_session_load_fn(r, zz); + (*zz)->remote_user = apr_table_get((*zz)->entries, OIDC_SESSION_REMOTE_USER_KEY); + const char *uuid = apr_table_get((*zz)->entries, OIDC_SESSION_UUID_KEY); + if (uuid != NULL) apr_uuid_parse((*zz)->uuid, uuid); + return rc; +} + +apr_status_t oidc_session_save(request_rec *r, session_rec *z) { + oidc_session_set(r, z, OIDC_SESSION_REMOTE_USER_KEY, z->remote_user); + char key[APR_UUID_FORMATTED_LENGTH + 1]; + apr_uuid_format((char *) &key, z->uuid); + oidc_session_set(r, z, OIDC_SESSION_UUID_KEY, key); + return ap_session_save_fn(r, z); +} + +apr_status_t oidc_session_get(request_rec *r, session_rec *z, const char *key, const char **value) { + return ap_session_get_fn(r, z, key, value); +} + +apr_status_t oidc_session_set(request_rec *r, session_rec *z, const char *key, const char *value) { + return ap_session_set_fn(r, z, key, value); +} + +apr_status_t oidc_session_kill(request_rec *r, session_rec *z) { + apr_table_clear(z->entries); + z->expiry = 0; + z->encoded = NULL; + return ap_session_save_fn(r, z); +} + +#ifndef OIDC_SESSION_USE_APACHE_SESSIONS + +// compatibility stuff copied from: +// http://contribsoft.caixamagica.pt/browser/internals/2012/apachecc/trunk/mod_session-port/src/util_port_compat.c +#define T_ESCAPE_URLENCODED (64) + +static const unsigned char test_c_table[256] = { 32, 126, 126, 126, 126, 126, + 126, 126, 126, 126, 127, 126, 126, 126, 126, 126, 126, 126, 126, 126, + 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 14, 64, 95, + 70, 65, 102, 65, 65, 73, 73, 1, 64, 72, 0, 0, 74, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 104, 79, 79, 72, 79, 79, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 95, 79, 71, 0, 71, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 79, 103, 79, 65, 126, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, 118, + 118, 118, 118, 118, 118, 118, 118 }; + +#define TEST_CHAR(c, f) (test_c_table[(unsigned)(c)] & (f)) + +static const char c2x_table[] = "0123456789abcdef"; + +static APR_INLINE unsigned char *c2x(unsigned what, unsigned char prefix, + unsigned char *where) { +#if APR_CHARSET_EBCDIC + what = apr_xlate_conv_byte(ap_hdrs_to_ascii, (unsigned char)what); +#endif /*APR_CHARSET_EBCDIC*/ + *where++ = prefix; + *where++ = c2x_table[what >> 4]; + *where++ = c2x_table[what & 0xf]; + return where; +} + +static char x2c(const char *what) { + register char digit; + +#if !APR_CHARSET_EBCDIC + digit = + ((what[0] >= 'A') ? ((what[0] & 0xdf) - 'A') + 10 : (what[0] - '0')); + digit *= 16; + digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A') + 10 : (what[1] - '0')); +#else /*APR_CHARSET_EBCDIC*/ + char xstr[5]; + xstr[0]='0'; + xstr[1]='x'; + xstr[2]=what[0]; + xstr[3]=what[1]; + xstr[4]='\0'; + digit = apr_xlate_conv_byte(ap_hdrs_from_ascii, + 0xFF & strtol(xstr, NULL, 16)); +#endif /*APR_CHARSET_EBCDIC*/ + return (digit); +} + +AP_DECLARE(char *) ap_escape_urlencoded_buffer(char *copy, const char *buffer) { + const unsigned char *s = (const unsigned char *) buffer; + unsigned char *d = (unsigned char *) copy; + unsigned c; + + while ((c = *s)) { + if (TEST_CHAR(c, T_ESCAPE_URLENCODED)) { + d = c2x(c, '%', d); + } else if (c == ' ') { + *d++ = '+'; + } else { + *d++ = c; + } + ++s; + } + *d = '\0'; + return copy; +} + +static int oidc_session_unescape_url(char *url, const char *forbid, + const char *reserved) { + register int badesc, badpath; + char *x, *y; + + badesc = 0; + badpath = 0; + /* Initial scan for first '%'. Don't bother writing values before + * seeing a '%' */ + y = strchr(url, '%'); + if (y == NULL) { + return OK; + } + for (x = y; *y; ++x, ++y) { + if (*y != '%') { + *x = *y; + } else { + if (!apr_isxdigit(*(y + 1)) || !apr_isxdigit(*(y + 2))) { + badesc = 1; + *x = '%'; + } else { + char decoded; + decoded = x2c(y + 1); + if ((decoded == '\0') + || (forbid && ap_strchr_c(forbid, decoded))) { + badpath = 1; + *x = decoded; + y += 2; + } else if (reserved && ap_strchr_c(reserved, decoded)) { + *x++ = *y++; + *x++ = *y++; + *x = *y; + } else { + *x = decoded; + y += 2; + } + } + } + } + *x = '\0'; + if (badesc) { + return HTTP_BAD_REQUEST; + } else if (badpath) { + return HTTP_NOT_FOUND; + } else { + return OK; + } +} + +AP_DECLARE(int) ap_unescape_urlencoded(char *query) { + char *slider; + /* replace plus with a space */ + if (query) { + for (slider = query; *slider; slider++) { + if (*slider == '+') { + *slider = ' '; + } + } + } + /* unescape everything else */ + return oidc_session_unescape_url(query, NULL, NULL); +} + +// copied from mod_session.c +static apr_status_t oidc_session_identity_decode(request_rec * r, + session_rec * z) { + char *last = NULL; + char *encoded, *pair; + const char *sep = "&"; + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_session_identity_decode: decoding %s", z->encoded); + + /* sanity check - anything to decode? */ + if (!z->encoded) { + return APR_SUCCESS; + } + + /* decode what we have */ + encoded = apr_pstrdup(r->pool, z->encoded); + pair = apr_strtok(encoded, sep, &last); + while (pair && pair[0]) { + char *plast = NULL; + const char *psep = "="; + char *key = apr_strtok(pair, psep, &plast); + char *val = apr_strtok(NULL, psep, &plast); + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_session_identity_decode: decoding %s=%s", key, val); + + if (key && *key) { + if (!val || !*val) { + apr_table_unset(z->entries, key); + } else if (!ap_unescape_urlencoded(key) + && !ap_unescape_urlencoded(val)) { + if (!strcmp(OIDC_SESSION_EXPIRY_KEY, key)) { + z->expiry = (apr_time_t) apr_atoi64(val); + } else { + apr_table_set(z->entries, key, val); + } + } + } + pair = apr_strtok(NULL, sep, &last); + } + z->encoded = NULL; + return APR_SUCCESS; +} + +// copied from mod_session.c +static int oidc_identity_count(int *count, const char *key, const char *val) { + *count += strlen(key) * 3 + strlen(val) * 3 + 1; + return 1; +} + +// copied from mod_session.c +static int oidc_identity_concat(char *buffer, const char *key, const char *val) { + char *slider = buffer; + int length = strlen(slider); + slider += length; + if (length) { + *slider = '&'; + slider++; + } + ap_escape_urlencoded_buffer(slider, key); + slider += strlen(slider); + *slider = '='; + slider++; + ap_escape_urlencoded_buffer(slider, val); + return 1; +} + +// copied from mod_session.c +static apr_status_t oidc_session_identity_encode(request_rec * r, + session_rec * z) { + char *buffer = NULL; + int length = 0; + if (z->expiry) { + char *expiry = apr_psprintf(z->pool, "%" APR_INT64_T_FMT, z->expiry); + apr_table_setn(z->entries, OIDC_SESSION_EXPIRY_KEY, expiry); + } + apr_table_do( + (int (*)(void *, const char *, const char *)) oidc_identity_count, + &length, z->entries, NULL); + buffer = apr_pcalloc(r->pool, length + 1); + apr_table_do( + (int (*)(void *, const char *, const char *)) oidc_identity_concat, + buffer, z->entries, NULL); + z->encoded = buffer; + return APR_SUCCESS; + +} + +/* load the session from the cache using the cookie as the index */ +static apr_status_t oidc_session_load_cache(request_rec *r, session_rec *z) { + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + oidc_dir_cfg *d = ap_get_module_config(r->per_dir_config, &auth_openidc_module); + + /* get the cookie that should be our uuid/key */ + char *uuid = oidc_get_cookie(r, d->cookie); + + /* get the string-encoded session from the cache based on the key */ + if (uuid != NULL) + if (c->cache->get(r, uuid, &z->encoded) == TRUE) return APR_SUCCESS; + + return APR_EGENERAL; +} + +/* + * save the session to the cache using a cookie for the index + */ +static apr_status_t oidc_session_save_cache(request_rec *r, session_rec *z) { + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + oidc_dir_cfg *d = ap_get_module_config(r->per_dir_config, &auth_openidc_module); + + char key[APR_UUID_FORMATTED_LENGTH + 1]; + apr_uuid_format((char *) &key, z->uuid); + + if (z->encoded && z->encoded[0]) { + + /* set the uuid in the cookie */ + oidc_set_cookie(r, d->cookie, key); + + /* store the string-encoded session in the cache */ + c->cache->set(r, key, z->encoded, z->expiry); + + } else { + + /* clear the cookie */ + oidc_set_cookie(r, d->cookie, ""); + + /* remove the session from the cache */ + c->cache->set(r, key, NULL, 0); + } + + return APR_SUCCESS; +} + +static apr_status_t oidc_session_load_cookie(request_rec *r, session_rec *z) { + oidc_dir_cfg *d = ap_get_module_config(r->per_dir_config, &auth_openidc_module); + + char *cookieValue = oidc_get_cookie(r, d->cookie); + if (cookieValue != NULL) { + if (oidc_base64url_decode_decrypt_string(r, (char **)&z->encoded, cookieValue) <= 0) return APR_EGENERAL; + } + + return APR_SUCCESS; +} + +static apr_status_t oidc_session_save_cookie(request_rec *r, session_rec *z) { + oidc_dir_cfg *d = ap_get_module_config(r->per_dir_config, &auth_openidc_module); + + char *cookieValue = ""; + if (z->encoded && z->encoded[0]) { + oidc_encrypt_base64url_encode_string(r, &cookieValue, z->encoded); + } + oidc_set_cookie(r, d->cookie, cookieValue); + + return APR_SUCCESS; +} + +/* + * load the session from the request context, create a new one if no luck + */ +static apr_status_t oidc_session_load_22(request_rec *r, session_rec **zz) { + + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + + /* first see if this is a sub-request and it was set already in the main request */ + if (((*zz) = (session_rec *) oidc_request_state_get(r, "session")) != NULL) { + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_session_load: loading session from request state"); + return APR_SUCCESS; + } + + /* allocate space for the session object and fill it */ + session_rec *z = (*zz = apr_pcalloc(r->pool, sizeof(session_rec))); + z->pool = r->pool; + + /* get a new uuid for this session */ + z->uuid = (apr_uuid_t *) apr_pcalloc(z->pool, sizeof(apr_uuid_t)); + apr_uuid_get(z->uuid); + + z->remote_user = NULL; + z->encoded = NULL; + z->entries = apr_table_make(z->pool, 10); + + apr_status_t rc = APR_SUCCESS; + if (c->session_type == OIDC_SESSION_TYPE_22_CACHE_FILE) { + /* load the session from the cache */ + rc = oidc_session_load_cache(r, z); + } else if (c->session_type == OIDC_SESSION_TYPE_22_COOKIE) { + /* load the session from a self-contained cookie */ + rc = oidc_session_load_cookie(r, z); + } else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_session_load_22: unknown session type: %d", + c->session_type); + rc = APR_EGENERAL; + } + + /* see if it worked out */ + if (rc != APR_SUCCESS) + return rc; + + /* yup, now decode the info */ + if (oidc_session_identity_decode(r, z) != APR_SUCCESS) + return APR_EGENERAL; + + /* check whether it has expired */ + if (apr_time_now() > z->expiry) { + + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_session_load_22: session restored from cache has expired"); + apr_table_clear(z->entries); + return APR_EGENERAL; + } + + /* store this session in the request context, so it is available to sub-requests */ + oidc_request_state_set(r, "session", (const char *) z); + + return APR_SUCCESS; +} + +/* + * save a session to the cache + */ +static apr_status_t oidc_session_save_22(request_rec *r, session_rec *z) { + + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + + /* encode the actual state in to the encoded string */ + oidc_session_identity_encode(r, z); + + /* store this session in the request context, so it is available to sub-requests as a quicker-than-file-backend cache */ + oidc_request_state_set(r, "session", (const char *) z); + + apr_status_t rc = APR_SUCCESS; + if (c->session_type == OIDC_SESSION_TYPE_22_CACHE_FILE) { + /* store the session in the cache */ + rc = oidc_session_save_cache(r, z); + } else if (c->session_type == OIDC_SESSION_TYPE_22_COOKIE) { + /* store the session in a self-contained cookie */ + rc = oidc_session_save_cookie(r, z); + } else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_session_save_22: unknown session type: %d", c->session_type); + rc = APR_EGENERAL; + } + + return rc; +} + +/* + * get a value from the session based on the name from a name/value pair + */ +static apr_status_t oidc_session_get_22(request_rec *r, session_rec *z, const char *key, + const char **value) { + + /* just return the value for the key */ + *value = apr_table_get(z->entries, key); + + return OK; +} + +/* + * set a name/value key pair in the session + */ +static apr_status_t oidc_session_set_22(request_rec *r, session_rec *z, const char *key, + const char *value) { + + /* only set it if non-NULL, otherwise delete the entry */ + if (value) { + apr_table_set(z->entries, key, value); + } else { + apr_table_unset(z->entries, key); + } + return OK; +} + +/* + * session initialization for pre-2.4 + */ +apr_status_t oidc_session_init() { + if (!ap_session_load_fn || !ap_session_get_fn || !ap_session_set_fn || !ap_session_save_fn) { + ap_session_load_fn = oidc_session_load_22; + ap_session_get_fn = oidc_session_get_22; + ap_session_set_fn = oidc_session_set_22; + ap_session_save_fn = oidc_session_save_22; + } + return OK; +} + +#else + +/* + * use Apache 2.4 session handling + */ + +#include + +apr_status_t oidc_session_init() { + if (!ap_session_load_fn || !ap_session_get_fn || !ap_session_set_fn || !ap_session_save_fn) { + ap_session_load_fn = APR_RETRIEVE_OPTIONAL_FN(ap_session_load); + ap_session_get_fn = APR_RETRIEVE_OPTIONAL_FN(ap_session_get); + ap_session_set_fn = APR_RETRIEVE_OPTIONAL_FN(ap_session_set); + ap_session_save_fn = APR_RETRIEVE_OPTIONAL_FN(ap_session_save); + } + return OK; +} + +#endif diff --git a/src/util.c b/src/util.c new file mode 100644 index 00000000..86b18307 --- /dev/null +++ b/src/util.c @@ -0,0 +1,1024 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2013-2014 Ping Identity Corporation + * All rights reserved. + * + * The contents of this file are the property of Ping Identity Corporation. + * For further information please contact: + * + * Ping Identity Corporation + * 1099 18th St Suite 2950 + * Denver, CO 80202 + * 303.468.2900 + * http://www.pingidentity.com + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @Author: Hans Zandbelt - hzandbelt@pingidentity.com + */ + +#include +#include +#include + +#include +#include +#include +#include +#include "http_protocol.h" + +#include + +#include "mod_auth_openidc.h" + +/* hrm, should we get rid of this by adding parameters to the (3) functions? */ +extern module AP_MODULE_DECLARE_DATA auth_openidc_module; + +/* + * base64url encode a string + */ +int oidc_base64url_encode(request_rec *r, char **dst, const char *src, + int src_len) { + // TODO: always padded now, do we need an option to remove the padding? + if ((src == NULL) || (src_len <= 0)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_base64url_encode: not encoding anything; src=NULL and/or src_len<1"); + return -1; + } + int enc_len = apr_base64_encode_len(src_len); + char *enc = apr_palloc(r->pool, enc_len); + apr_base64_encode(enc, (const char *) src, src_len); + int i = 0; + while (enc[i] != '\0') { + if (enc[i] == '+') + enc[i] = '-'; + if (enc[i] == '/') + enc[i] = '_'; + if (enc[i] == '=') + enc[i] = ','; + i++; + } + *dst = enc; + return enc_len; +} + +/* + * base64url decode a string + */ +int oidc_base64url_decode(request_rec *r, char **dst, const char *src, + int padding) { + // TODO: check base64url decoding/encoding code and look for alternatives? + if (src == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_base64url_decode: not decoding anything; src=NULL"); + return -1; + } + char *dec = apr_pstrdup(r->pool, src); + int i = 0; + while (dec[i] != '\0') { + if (dec[i] == '-') + dec[i] = '+'; + if (dec[i] == '_') + dec[i] = '/'; + if (dec[i] == ',') + dec[i] = '='; + i++; + } + if (padding == 1) { + switch (strlen(dec) % 4) { + case 0: + break; + case 2: + dec = apr_pstrcat(r->pool, dec, "==", NULL); + break; + case 3: + dec = apr_pstrcat(r->pool, dec, "=", NULL); + break; + default: + return 0; + } + } + int dlen = apr_base64_decode_len(dec); + *dst = apr_palloc(r->pool, dlen); + return apr_base64_decode(*dst, dec); +} + +/* + * encrypt and base64url encode a string + */ +int oidc_encrypt_base64url_encode_string(request_rec *r, char **dst, + const char *src) { + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + int crypted_len = strlen(src) + 1; + unsigned char *crypted = oidc_crypto_aes_encrypt(r, c, + (unsigned char *) src, &crypted_len); + if (crypted == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_encrypt_base64url_encode_string: oidc_crypto_aes_encrypt failed"); + return -1; + } + return oidc_base64url_encode(r, dst, (const char *) crypted, crypted_len); +} + +/* + * decrypt and base64url decode a string + */ +int oidc_base64url_decode_decrypt_string(request_rec *r, char **dst, + const char *src) { + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + char *decbuf = NULL; + int dec_len = oidc_base64url_decode(r, &decbuf, src, 0); + if (dec_len <= 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_base64url_decode_decrypt_string: oidc_base64url_decode failed"); + return -1; + } + *dst = (char *) oidc_crypto_aes_decrypt(r, c, (unsigned char *) decbuf, + &dec_len); + if (*dst == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_base64url_decode_decrypt_string: oidc_crypto_aes_decrypt failed"); + return -1; + } + return dec_len; +} + +/* + * convert a character to an ENVIRONMENT-variable-safe variant + */ +int oidc_char_to_env(int c) { + return apr_isalnum(c) ? apr_toupper(c) : '_'; +} + +/* + * compare two strings based on how they would be converted to an + * environment variable, as per oidc_char_to_env. If len is specified + * as less than zero, then the full strings will be compared. Returns + * less than, equal to, or greater than zero based on whether the + * first argument's conversion to an environment variable is less + * than, equal to, or greater than the second. + */ +int oidc_strnenvcmp(const char *a, const char *b, int len) { + int d, i = 0; + while (1) { + /* If len < 0 then we don't stop based on length */ + if (len >= 0 && i >= len) + return 0; + + /* If we're at the end of both strings, they're equal */ + if (!*a && !*b) + return 0; + + /* If the second string is shorter, pick it: */ + if (*a && !*b) + return 1; + + /* If the first string is shorter, pick it: */ + if (!*a && *b) + return -1; + + /* Normalize the characters as for conversion to an + * environment variable. */ + d = oidc_char_to_env(*a) - oidc_char_to_env(*b); + if (d) + return d; + + a++; + b++; + i++; + } + return 0; +} + +/* + * escape a string + */ +char *oidc_util_escape_string(const request_rec *r, const char *str) { + CURL *curl = curl_easy_init(); + if (curl == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_escape_string: curl_easy_init() error"); + return NULL; + } + char *result = curl_easy_escape(curl, str, 0); + if (result == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_escape_string: curl_easy_escape() error"); + return NULL; + } + char *rv = apr_pstrdup(r->pool, result); + curl_free(result); + curl_easy_cleanup(curl); + return rv; +} + +/* + * escape a string + */ +char *oidc_util_unescape_string(const request_rec *r, const char *str) { + CURL *curl = curl_easy_init(); + if (curl == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_unescape_string: curl_easy_init() error"); + return NULL; + } + char *result = curl_easy_unescape(curl, str, 0, 0); + if (result == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_unescape_string: curl_easy_unescape() error"); + return NULL; + } + char *rv = apr_pstrdup(r->pool, result); + curl_free(result); + curl_easy_cleanup(curl); + //ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_util_unescape_string: input=\"%s\", output=\"%s\"", str, rv); + return rv; +} + +/* + * get the URL that is currently being accessed + * TODO: seems hard enough, maybe look for other existing code...? + */ +char *oidc_get_current_url(const request_rec *r, const oidc_cfg *c) { + const apr_port_t port = r->connection->local_addr->port; + char *scheme, *port_str = "", *url; + apr_byte_t print_port = TRUE; +#ifdef APACHE2_0 + scheme = (char *) ap_http_method(r); +#else + scheme = (char *) ap_http_scheme(r); +#endif + if ((apr_strnatcmp(scheme, "https") == 0) && port == 443) + print_port = FALSE; + else if ((apr_strnatcmp(scheme, "http") == 0) && port == 80) + print_port = FALSE; + if (print_port) + port_str = apr_psprintf(r->pool, ":%u", port); + url = apr_pstrcat(r->pool, scheme, "://", + apr_table_get(r->headers_in, "Host"), port_str, r->uri, + (r->args != NULL && *r->args != '\0' ? "?" : ""), r->args, NULL); + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_get_current_url: current URL '%s'", url); + return url; +} + +/* maximum size of any response returned in HTTP calls */ +#define OIDC_CURL_MAX_RESPONSE_SIZE 65536 + +/* buffer to hold HTTP call responses */ +typedef struct oidc_curl_buffer { + char buf[OIDC_CURL_MAX_RESPONSE_SIZE]; + size_t written; +} oidc_curl_buffer; + +/* + * callback for CURL to write bytes that come back from an HTTP call + */ +size_t oidc_curl_write(const void *ptr, size_t size, size_t nmemb, void *stream) { + oidc_curl_buffer *curlBuffer = (oidc_curl_buffer *) stream; + + if ((nmemb * size) + curlBuffer->written >= OIDC_CURL_MAX_RESPONSE_SIZE) + return 0; + + memcpy((curlBuffer->buf + curlBuffer->written), ptr, (nmemb*size)); + curlBuffer->written += (nmemb * size); + + return (nmemb * size); +} + +/* context structure for encoding parameters */ +typedef struct oidc_http_encode_t { + request_rec *r; + const char *encoded_params; +} oidc_http_encode_t; + +/* + * add a url-form-encoded name/value pair + */ +static int oidc_http_add_form_url_encoded_param(void* rec, const char* key, + const char* value) { + // TODO: handle arrays of strings? + oidc_http_encode_t *ctx = (oidc_http_encode_t*) rec; + const char *sep = apr_strnatcmp(ctx->encoded_params, "") == 0 ? "" : "&"; + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, ctx->r, + "oidc_http_add_post_param: adding parameter: %s=%s to %s (sep=%s)", + key, value, ctx->encoded_params, sep); + ctx->encoded_params = apr_psprintf(ctx->r->pool, "%s%s%s=%s", + ctx->encoded_params, sep, oidc_util_escape_string(ctx->r, key), + oidc_util_escape_string(ctx->r, value)); + return 1; +} + +/* + * add a JSON name/value pair + */ +static int oidc_http_add_json_param(void* rec, const char* key, + const char* value) { + oidc_http_encode_t *ctx = (oidc_http_encode_t*) rec; + const char *sep = apr_strnatcmp(ctx->encoded_params, "") == 0 ? "" : ","; + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, ctx->r, + "oidc_http_add_json_param: adding parameter: %s=%s to %s", key, + value, ctx->encoded_params); + if (value[0] == '[') { + // TODO hacky hacky, we need an array so we already encoded it :-) + ctx->encoded_params = apr_psprintf(ctx->r->pool, "%s%s\"%s\" : %s", + ctx->encoded_params, sep, key, value); + } else { + ctx->encoded_params = apr_psprintf(ctx->r->pool, "%s%s\"%s\": \"%s\"", + ctx->encoded_params, sep, key, value); + } + return 1; +} + +/* + * execute a HTTP (GET or POST) request + */ +apr_byte_t oidc_util_http_call(request_rec *r, const char *url, int action, + const apr_table_t *params, const char *basic_auth, + const char *bearer_token, int ssl_validate_server, + const char **response, int timeout) { + char curlError[CURL_ERROR_SIZE]; + oidc_curl_buffer curlBuffer; + CURL *curl; + struct curl_slist *h_list = NULL; + int nr_of_params = (params != NULL) ? apr_table_elts(params)->nelts : 0; + + /* do some logging about the inputs */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_util_http_call: entering, url=%s, action=%d, #params=%d, basic_auth=%s, bearer_token=%s, ssl_validate_server=%d", + url, action, nr_of_params, basic_auth, bearer_token, + ssl_validate_server); + + curl = curl_easy_init(); + if (curl == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_http_call: curl_easy_init() error"); + return FALSE; + } + + /* some of these are not really required */ + curl_easy_setopt(curl, CURLOPT_HEADER, 0L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curlError); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + + /* set the timeout */ + curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout); + + /* setup the buffer where the response will be written to */ + curlBuffer.written = 0; + memset(curlBuffer.buf, '\0', sizeof(curlBuffer.buf)); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &curlBuffer); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, oidc_curl_write); + +#ifndef LIBCURL_NO_CURLPROTO + curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, + CURLPROTO_HTTP|CURLPROTO_HTTPS); + curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS); +#endif + + /* set the options for validating the SSL server certificate that the remote site presents */ + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, + (ssl_validate_server != FALSE ? 1L : 0L)); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, + (ssl_validate_server != FALSE ? 2L : 0L)); + + /* identify this HTTP client */ + curl_easy_setopt(curl, CURLOPT_USERAGENT, "mod_auth_openidc"); + + /* see if we need to add token in the Bearer Authorization header */ + if (bearer_token != NULL) { + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, + apr_psprintf(r->pool, "Authorization: Bearer %s", + bearer_token)); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } + + /* see if we need to perform HTTP basic authentication to the remote site */ + if (basic_auth != NULL) { + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(curl, CURLOPT_USERPWD, basic_auth); + } + + /* the POST contents */ + oidc_http_encode_t data = { r, "" }; + + if (action == OIDC_HTTP_POST_JSON) { + + /* POST JSON data */ + + if (nr_of_params > 0) { + + /* add the parameters in JSON formatting */ + apr_table_do(oidc_http_add_json_param, &data, params, NULL); + /* surround it by brackets to make it a valid JSON object */ + data.encoded_params = apr_psprintf(r->pool, "{ %s }", + data.encoded_params); + + /* set the data and log the event */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_util_http_call: setting JSON parameters: %s", + data.encoded_params); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.encoded_params); + } + + /* set HTTP method to POST */ + curl_easy_setopt(curl, CURLOPT_POST, 1); + + /* and overwrite the default url-form-encoded content-type */ +// h_list = curl_slist_append(h_list, +// "Content-type: application/json; charset=UTF-8"); + h_list = curl_slist_append(h_list, + "Content-type: application/json"); + + } else if (action == OIDC_HTTP_POST_FORM) { + + /* POST url-form-encoded data */ + + if (nr_of_params > 0) { + + apr_table_do(oidc_http_add_form_url_encoded_param, &data, params, + NULL); + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_util_http_call: setting post parameters: %s", + data.encoded_params); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.encoded_params); + } // else: probably should warn here... + + /* CURLOPT_POST needed at least to set: Content-Type: application/x-www-form-urlencoded */ + curl_easy_setopt(curl, CURLOPT_POST, 1); + + } else if (nr_of_params > 0) { + + /* HTTP GET with #params > 0 */ + + apr_table_do(oidc_http_add_form_url_encoded_param, &data, params, NULL); + const char *sep = strchr(url, '?') != NULL ? "&" : "?"; + url = apr_psprintf(r->pool, "%s%s%s", url, sep, data.encoded_params); + + /* log that the URL has changed now */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_util_http_call: added query parameters to URL: %s", url); + } + + /* see if we need to add any custom headers */ + if (h_list != NULL) + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, h_list); + + /* set the target URL */ + curl_easy_setopt(curl, CURLOPT_URL, url); + + /* call it and record the result */ + int rv = TRUE; + if (curl_easy_perform(curl) != CURLE_OK) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_http_call: curl_easy_perform() failed on: %s (%s)", + url, curlError); + rv = FALSE; + goto out; + } + + *response = apr_pstrndup(r->pool, curlBuffer.buf, curlBuffer.written); + + /* set and log the response */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_util_http_call: response=%s", *response); + + out: + + /* cleanup and return the result */ + if (h_list != NULL) + curl_slist_free_all(h_list); + curl_easy_cleanup(curl); + + return rv; +} + +/* + * set a cookie in the HTTP response headers + */ +void oidc_set_cookie(request_rec *r, const char *cookieName, const char *cookieValue) { + + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + char *headerString, *currentCookies; + + /* construct the cookie value */ + headerString = apr_psprintf(r->pool, "%s=%s;Secure;Path=%s%s", cookieName, + cookieValue, oidc_get_cookie_path(r), + c->cookie_domain != NULL ? + apr_psprintf(r->pool, ";Domain=%s", c->cookie_domain) : ""); + + /* see if we need to clear the cookie */ + if (apr_strnatcmp(cookieValue, "") == 0) + headerString = apr_psprintf(r->pool, "%s;expires=0;Max-Age=0", + headerString); + + /* use r->err_headers_out so we always print our headers (even on 302 redirect) - headers_out only prints on 2xx responses */ + apr_table_add(r->err_headers_out, "Set-Cookie", headerString); + + /* see if we need to add it to existing cookies */ + if ((currentCookies = (char *) apr_table_get(r->headers_in, "Cookie")) + == NULL) + apr_table_add(r->headers_in, "Cookie", headerString); + else + apr_table_set(r->headers_in, "Cookie", + (apr_pstrcat(r->pool, headerString, ";", currentCookies, NULL))); + + /* do some logging */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_set_cookie: adding outgoing header: Set-Cookie: %s", + headerString); +} + +/* + * get a cookie from the HTTP request + */ +char *oidc_get_cookie(request_rec *r, char *cookieName) { + char *cookie, *tokenizerCtx, *rv = NULL; + + /* get the Cookie value */ + char *cookies = apr_pstrdup(r->pool, + (char *) apr_table_get(r->headers_in, "Cookie")); + + if (cookies != NULL) { + + /* tokenize on ; to find the cookie we want */ + cookie = apr_strtok(cookies, ";", &tokenizerCtx); + + do { + + while (cookie != NULL && *cookie == ' ') + cookie++; + + /* see if we've found the cookie that we're looking for */ + if (strncmp(cookie, cookieName, strlen(cookieName)) == 0) { + + /* skip to the meat of the parameter (the value after the '=') */ + cookie += (strlen(cookieName) + 1); + rv = apr_pstrdup(r->pool, cookie); + + break; + } + + /* go to the next cookie */ + cookie = apr_strtok(NULL, ";", &tokenizerCtx); + + } while (cookie != NULL); + } + + /* log what we've found */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, "oidc_get_cookie: returning %s", + rv); + + return rv; +} + +/* + * normalize a string for use as an HTTP Header Name. Any invalid + * characters (per http://tools.ietf.org/html/rfc2616#section-4.2 and + * http://tools.ietf.org/html/rfc2616#section-2.2) are replaced with + * a dash ('-') character. + */ +char *oidc_normalize_header_name(const request_rec *r, const char *str) { + /* token = 1* + * CTL = + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT */ + const char *separators = "()<>@,;:\\\"/[]?={} \t"; + + char *ns = apr_pstrdup(r->pool, str); + size_t i; + for (i = 0; i < strlen(ns); i++) { + if (ns[i] < 32 || ns[i] == 127) + ns[i] = '-'; + else if (strchr(separators, ns[i]) != NULL) + ns[i] = '-'; + } + return ns; +} + +/* + * see if the currently accessed path matches a path from a defined URL + */ +apr_byte_t oidc_util_request_matches_url(request_rec *r, const char *url) { + apr_uri_t uri; + apr_uri_parse(r->pool, url, &uri); + apr_byte_t rc = + (apr_strnatcmp(r->parsed_uri.path, uri.path) == 0) ? TRUE : FALSE; + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_request_matches_url: comparing \"%s\"==\"%s\" (%d)", + r->parsed_uri.path, uri.path, rc); + return rc; +} + +/* + * see if the currently accessed path has a certain query parameter + */ +apr_byte_t oidc_util_request_has_parameter(request_rec *r, const char* param) { + if (r->args == NULL) + return FALSE; + const char *option1 = apr_psprintf(r->pool, "%s=", param); + const char *option2 = apr_psprintf(r->pool, "&%s=", param); + return ((strstr(r->args, option1) == r->args) + || (strstr(r->args, option2) != NULL)) ? TRUE : FALSE; +} + +/* + * get a query parameter + */ +apr_byte_t oidc_util_get_request_parameter(request_rec *r, char *name, + char **value) { + // TODO: we should really check with ? and & and avoid any code= stuff to trigger true + char *tokenizer_ctx, *p, *args; + const char *k_param = apr_psprintf(r->pool, "%s=", name); + const size_t k_param_sz = strlen(k_param); + + *value = NULL; + + if (r->args == NULL || strlen(r->args) == 0) + return FALSE; + + /* not sure why we do this, but better be safe than sorry */ + args = apr_pstrndup(r->pool, r->args, strlen(r->args)); + + p = apr_strtok(args, "&", &tokenizer_ctx); + do { + if (p && strncmp(p, k_param, k_param_sz) == 0) { + *value = apr_pstrdup(r->pool, p + k_param_sz); + *value = oidc_util_unescape_string(r, *value); + } + p = apr_strtok(NULL, "&", &tokenizer_ctx); + } while (p); + + return (*value != NULL ? TRUE : FALSE); +} + +/* + * printout a JSON string value + */ +static apr_byte_t oidc_util_json_string_print(request_rec *r, + apr_json_value_t *result, const char *key, const char *log) { + apr_json_value_t *value = apr_hash_get(result->value.object, key, + APR_HASH_KEY_STRING); + if (value != NULL) { + if (value->type == APR_JSON_STRING) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "%s: response contained a \"%s\" key with string value: \"%s\"", + log, key, value->value.string.p); + } else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "%s: response contained an \"%s\" key but no string value", + log, key); + } + return TRUE; + } + return FALSE; +} + +/* + * check a JSON object for "error" results and printout + */ +static apr_byte_t oidc_util_check_json_error(request_rec *r, + apr_json_value_t *json) { + if (oidc_util_json_string_print(r, json, "error", + "oidc_util_check_json_error") == TRUE) { + oidc_util_json_string_print(r, json, "error_description", + "oidc_util_check_json_error"); + return TRUE; + } + return FALSE; +} + +/* + * decode a JSON string, check for "error" results and printout + */ +apr_byte_t oidc_util_decode_json_and_check_error(request_rec *r, + const char *str, apr_json_value_t **json) { + + /* decode the JSON contents of the buffer */ + if (apr_json_decode(json, str, strlen(str), r->pool) != APR_SUCCESS) { + /* something went wrong */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_check_json_error: JSON parsing returned an error"); + return FALSE; + } + + if ((*json == NULL) || ((*json)->type != APR_JSON_OBJECT)) { + /* oops, no JSON */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_check_json_error: parsed JSON did not contain a JSON object"); + return FALSE; + } + + // see if it is not an error response somehow + if (oidc_util_check_json_error(r, *json) == TRUE) + return FALSE; + + return TRUE; +} + +/* + * sends HTML content to the user agent + */ +int oidc_util_http_sendstring(request_rec *r, const char *html, int success_rvalue) { + ap_set_content_type(r, "text/html"); + apr_bucket_brigade *bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + apr_bucket *b = apr_bucket_transient_create(html, strlen(html), r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + b = apr_bucket_eos_create(r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS) + return HTTP_INTERNAL_SERVER_ERROR; + //r->status = success_rvalue; + return success_rvalue; +} + +int oidc_base64url_decode_rsa_verify(request_rec *r, const char *alg, const char *signature, const char *message, const char *modulus, const char *exponent) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_base64url_decode_rsa_verify: alg = \"%s\"", alg); + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_base64url_decode_rsa_verify: signature = \"%s\"", signature); + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_base64url_decode_rsa_verify: message = \"%s\"", message); + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_base64url_decode_rsa_verify: modulus = \"%s\"", modulus); + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_base64url_decode_rsa_verify: exponent = \"%s\"", exponent); + + unsigned char *mod = NULL; + int mod_len = oidc_base64url_decode(r, (char **)&mod, modulus, 1); + + unsigned char *exp = NULL; + int exp_len = oidc_base64url_decode(r, (char **)&exp, exponent, 1); + + unsigned char *sig = NULL; + int sig_len = oidc_base64url_decode(r, (char **)&sig, signature, 1); + + return oidc_crypto_rsa_verify(r, alg, sig, sig_len, (unsigned char *)message, strlen(message), mod, mod_len, exp, exp_len); +} + +/* + * read all bytes from the HTTP request + */ +static apr_byte_t oidc_util_read(request_rec *r, const char **rbuf) { + + if (ap_setup_client_block(r, REQUEST_CHUNKED_ERROR) != OK) + return FALSE; + + if (ap_should_client_block(r)) { + + char argsbuffer[HUGE_STRING_LEN]; + int rsize, len_read, rpos = 0; + long length = r->remaining; + *rbuf = apr_pcalloc(r->pool, length + 1); + + while ((len_read = ap_get_client_block(r, argsbuffer, + sizeof(argsbuffer))) > 0) { + if ((rpos + len_read) > length) { + rsize = length - rpos; + } else { + rsize = len_read; + } + memcpy((char*)*rbuf + rpos, argsbuffer, rsize); + rpos += rsize; + } + } + + return TRUE; +} + +/* + * read the POST parameters in to a table + */ + apr_byte_t oidc_util_read_post(request_rec *r, apr_table_t *table) { + const char *data = NULL; + const char *key, *val; + + if (r->method_number != M_POST) + return FALSE; + + if (oidc_util_read(r, &data) != TRUE) + return FALSE; + + while (data && *data && (val = ap_getword(r->pool, &data, '&'))) { + key = ap_getword(r->pool, &val, '='); + key = oidc_util_unescape_string(r, key); + val = oidc_util_unescape_string(r, val); + //ap_unescape_url((char*) key); + //ap_unescape_url((char*) val); + apr_table_set(table, key, val); + } + + return TRUE; +} + + // TODO: check return values + apr_byte_t oidc_util_generate_random_base64url_encoded_value(request_rec *r, int randomLen, char **randomB64) { + unsigned char *brnd = apr_pcalloc(r->pool, randomLen); + apr_generate_random_bytes((unsigned char *) brnd, randomLen); + *randomB64 = apr_palloc(r->pool, apr_base64_encode_len(randomLen) + 1); + char *enc = *randomB64; + apr_base64_encode(enc, (const char *) brnd, randomLen); + int i = 0; + while (enc[i] != '\0') { + if (enc[i] == '+') + enc[i] = '-'; + if (enc[i] == '/') + enc[i] = '_'; + if (enc[i] == '=') + enc[i] = ','; + i++; + } + return TRUE; + } + +/* + * read a file from a path on disk + */ +apr_byte_t oidc_util_file_read(request_rec *r, const char *path, + char **result) { + apr_file_t *fd = NULL; + apr_status_t rc = APR_SUCCESS; + char s_err[128]; + apr_finfo_t finfo; + + /* open the file if it exists */ + if ((rc = apr_file_open(&fd, path, APR_FOPEN_READ | APR_FOPEN_BUFFERED, + APR_OS_DEFAULT, r->pool)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "oidc_util_file_read: no file found at: \"%s\"", + path); + return FALSE; + } + + /* the file exists, now lock it */ + apr_file_lock(fd, APR_FLOCK_EXCLUSIVE); + + /* move the read pointer to the very start of the cache file */ + apr_off_t begin = 0; + apr_file_seek(fd, APR_SET, &begin); + + /* get the file info so we know its size */ + if ((rc = apr_file_info_get(&finfo, APR_FINFO_SIZE, fd)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_file_read: error calling apr_file_info_get on file: \"%s\" (%s)", + path, apr_strerror(rc, s_err, sizeof(s_err))); + goto error_close; + } + + /* now that we have the size of the file, allocate a buffer that can contain its contents */ + *result = apr_palloc(r->pool, finfo.size + 1); + + /* read the file in to the buffer */ + apr_size_t bytes_read = 0; + if ((rc = apr_file_read_full(fd, *result, finfo.size, &bytes_read)) + != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_file_read: apr_file_read_full on (%s) returned an error: %s", + path, apr_strerror(rc, s_err, sizeof(s_err))); + goto error_close; + } + + /* just to be sure, we set a \0 (we allocated space for it anyway) */ + (*result)[bytes_read] = '\0'; + + /* check that we've got all of it */ + if (bytes_read != finfo.size) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_file_read: apr_file_read_full on (%s) returned less bytes (%" APR_SIZE_T_FMT ") than expected: (%" APR_OFF_T_FMT ")", + path, bytes_read, finfo.size); + goto error_close; + } + + /* we're done, unlock and close the file */ + apr_file_unlock(fd); + apr_file_close(fd); + + /* log successful content retrieval */ + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_util_file_read: file read successfully \"%s\"", path); + + return TRUE; + +error_close: + + apr_file_unlock(fd); + apr_file_close(fd); + + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_file_read: returning error"); + + return FALSE; +} + +/* + * see if two provided issuer identifiers match (cq. ignore trailing slash) + */ +apr_byte_t oidc_util_issuer_match(const char *a, const char *b) { + + /* check the "issuer" value against the one configure for the provider we got this id_token from */ + if (strcmp(a, b) != 0) { + + /* no strict match, but we are going to accept if the difference is only a trailing slash */ + int n1 = strlen(a); + int n2 = strlen(b); + int n = ((n1 == n2 + 1) && (a[n1 - 1] == '/')) ? + n2 : (((n2 == n1 + 1) && (b[n2 - 1] == '/')) ? n1 : 0); + if ((n == 0) || (strncmp(a, b, n) != 0)) + return FALSE; + } + + return TRUE; +} + +/* + * send a user-facing error to the browser + * TODO: more templating + */ +int oidc_util_html_send_error(request_rec *r, const char *error, const char *description, int status_code) { + char *msg = "

the OpenID Connect Provider returned an error:

"; + + if (error != NULL) { + msg = apr_psprintf(r->pool, "%s

Error:

%s

", msg, + error); + } + if (description != NULL) { + msg = apr_psprintf(r->pool, "%s

Description:

%s

", + msg, description); + } + + return oidc_util_http_sendstring(r, msg, status_code); +} + +/* + * see if a certain string value is part of a JSON array with string elements + */ +apr_byte_t oidc_util_json_array_has_value(request_rec *r, + apr_json_value_t *haystack, const char *needle) { + + ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, + "oidc_util_json_array_has_value: entering (%s)", needle); + + if ( (haystack == NULL) || (haystack->type != APR_JSON_ARRAY) ) return FALSE; + + int i; + for (i = 0; i < haystack->value.array->nelts; i++) { + apr_json_value_t *elem = APR_ARRAY_IDX(haystack->value.array, i, + apr_json_value_t *); + if (elem->type != APR_JSON_STRING) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "oidc_util_json_array_has_value: unhandled in-array JSON non-string object type [%d]", + elem->type); + continue; + } + if (strcmp(elem->value.string.p, needle) == 0) { + break; + } + } + +// ap_log_rerror(APLOG_MARK, OIDC_DEBUG, 0, r, +// "oidc_util_json_array_has_value: returning (%d=%d)", i, +// haystack->value.array->nelts); + + return (i == haystack->value.array->nelts) ? FALSE : TRUE; +} diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..087313a4 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +/jmeter.log diff --git a/test/mod_auth_openidc.jmx b/test/mod_auth_openidc.jmx new file mode 100644 index 00000000..23e9f9b5 --- /dev/null +++ b/test/mod_auth_openidc.jmx @@ -0,0 +1,788 @@ + + + + + + false + false + + + + APP_SERVER + localhost.pingidentity.nl + = + + + APP_PORT + 443 + = + + + APP_PATH_USER + /protected/index.php?param=sample + = + + + AS_HOST + localhost + = + + + AS_PORT + 9031 + = + + + AS_PATH_TOKEN + /as/token.oauth2 + = + + + AS_CLIENT_ID + ro_client + false + = + + + AS_USERNAME + joe + false + = + + + AS_PASSWORD + 2Federate + false + = + + + APP_PATH_ACCESS + /protected2/ + = + + + + + + + + stoptest + + false + 10 + + 25 + 0 + 1299167831000 + 1299167831000 + false + + + + + + + + + true + piet + = + true + jan + + + + ${APP_SERVER} + ${APP_PORT} + + + https + + ${APP_PATH_USER} + GET + false + false + true + false + HttpClient4 + false + + + + + true + AuthorizationRequestPath + Location: https://(.*?)/(.*) + $2$ + REGEX_FAILED + 1 + + + + + + + + ${AS_HOST} + ${AS_PORT} + + + https + + /${AuthorizationRequestPath} + GET + false + false + true + false + HttpClient4 + false + + + + + false + AuthzResumePath + <form method=\"GET\" action=\"(.+)\"> + $1$ + REGEX_FAILED + 1 + + + + + + + + true + OTIdPJava + = + true + pfidpadapterid + + + + ${AS_HOST} + ${AS_PORT} + + + https + + ${AuthzResumePath} + GET + true + false + true + false + HttpClient4 + false + + + + + false + ResumeParam + <input type=\"hidden\" name=\"resume\" value=\"(.+)\"> + $1$ + REGEX_FAILED + 1 + + + + + + + + true + ${ResumeParam} + = + true + resume + + + true + ${USERNAME} + = + true + userName + + + true + ${PASSWORD} + = + true + password + + + + ${AS_HOST} + ${AS_PORT} + + + https + + /IdpSample/MainPage?cmd=login + POST + false + false + true + false + HttpClient4 + false + + + + + true + ResumePath + Location: https://(.*?)/(.*) + $2$ + REGEX_FAILED + 1 + + + + + + + + ${AS_HOST} + ${AS_PORT} + + + https + + ${ResumePath} + POST + false + false + true + false + HttpClient4 + false + + + + + true + RedirectURIPath + Location: https://(.*?)/(.*)# + $2$ + REGEX_FAILED + 1 + + + + true + StateFragment + Location: https://(.*?)#(.*?)state=(.*?)& + $3$ + REGEX_FAILED + 1 + + + + true + IDTokenFragment + Location: https://(.*?)#(.*?)id_token=(.*) + $3$ + REGEX_FAILED + 1 + + + + + + + + false + ${StateFragment} + = + true + state + + + false + ${IDTokenFragment} + = + true + id_token + + + + ${APP_SERVER} + ${APP_PORT} + + + https + + /${RedirectURIPath} + POST + false + false + true + false + HttpClient4 + false + + + + + true + ApplicationPath + Location: https://(.*?)/(.*) + $2$ + REGEX_FAILED + 1 + + + + + + + + true + ${ResumeParam} + = + true + resume + + + true + ${USERNAME} + = + true + userName + + + true + ${PASSWORD} + = + true + password + + + + ${AS_HOST} + ${AS_PORT} + + + https + + /IdpSample/MainPage?cmd=login + POST + true + false + true + false + HttpClient4 + false + + + + + false + cSRFTokenParam + <input type=\"hidden\" name=\"cSRFToken\" value=\"(.+)\"/> + $1$ + REGEX_FAILED + 1 + + + + + + + + true + true + = + true + check-user-approved-scope + + + true + openid + = + true + scope + + + true + email + = + true + scope + + + true + profile + = + true + scope + + + true + ${cSRFTokenParam} + = + true + cSRFToken + + + true + allow + = + true + pf.oauth.authz.consent + + + + ${AS_HOST} + ${AS_PORT} + + + https + + ${AuthzResumePath} + POST + false + false + true + false + HttpClient4 + false + + + + + true + LocationPath + Location: https://(.*?)/(.*) + $2$ + REGEX_FAILED + 1 + + + + + + + + ${APP_SERVER} + ${APP_PORT} + + + https + + /${LocationPath} + POST + false + false + true + false + HttpClient4 + false + + + + + true + ApplicationPath + Location: https://(.*?)/(.*) + $2$ + REGEX_FAILED + 1 + + + + + + + OIDC_CLAIM_BOGUS + bogus + + + + + + true + 10 + + + + + + + ${APP_SERVER} + ${APP_PORT} + + + https + + /${ApplicationPath} + GET + false + false + true + false + HttpClient4 + false + + + + + + \[OIDC_CLAIM_sub\] => ${USERNAME} + + Assertion.response_data + false + 2 + + + + + \[OIDC_CLAIM_BOGUS\] => bogus + + Assertion.response_data + false + 6 + + + + + + + true + rfc2109 + + + + , + + users.txt + false + true + shareMode.all + false + USERNAME,PASSWORD + + + + + stoptest + + false + 10 + + 25 + 0 + 1388176686000 + 1388176686000 + false + + + + + + + + + false + ${AS_CLIENT_ID} + = + true + client_id + + + false + password + = + true + grant_type + + + false + ${AS_USERNAME} + = + true + username + + + false + ${AS_PASSWORD} + = + true + password + + + + ${AS_HOST} + ${AS_PORT} + + + https + + ${AS_PATH_TOKEN} + POST + false + false + true + false + false + + + + + false + AccessToken + \"access_token\":\"(.+)\" + $1$ + REGEX_FAILED + 1 + + + + + + + Authorization + bearer ${AccessToken} + + + + + + true + 10 + + + + + + + ${APP_SERVER} + ${APP_PORT} + + + https + + ${APP_PATH_ACCESS} + GET + false + false + true + false + false + + + + + + ${AS_USERNAME}\*\*\* + + Assertion.response_data + false + 2 + + + + + + + true + + saveConfig + + + true + true + true + + true + true + true + true + false + false + false + false + false + false + true + false + false + false + false + 0 + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + false + false + false + false + false + 0 + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + false + false + false + false + false + 0 + true + + + + + + + + diff --git a/test/users.txt b/test/users.txt new file mode 100644 index 00000000..caab3c51 --- /dev/null +++ b/test/users.txt @@ -0,0 +1,3 @@ +joe,test +sarah,test +idp,test