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
\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);
+
+ /* 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"
+ "