From fff2a454fa2b0fe38c90add5d75e450564bd6c79 Mon Sep 17 00:00:00 2001
From: Hans Zandbelt
Date: Thu, 27 Mar 2014 20:18:52 +0100
Subject: [PATCH] initial import named mod_auth_openidc
---
.gitignore | 9 +
ChangeLog | 2 +
LICENSE.txt | 202 +++
Makefile.in | 70 ++
README | 150 +++
autogen.sh | 3 +
configure.ac | 55 +
debian/auth_openidc.conf | 277 +++++
debian/auth_openidc.load | 1 +
debian/changelog | 5 +
debian/compat | 1 +
debian/control | 16 +
debian/copyright | 28 +
debian/dirs | 3 +
debian/docs | 2 +
debian/libapache2-mod-auth-openidc.apache2 | 3 +
debian/lintian-overrides | 2 +
debian/rules | 23 +
debian/source/format | 1 +
src/.gitignore | 5 +
src/apr_json.h | 156 +++
src/apr_json_decode.c | 695 +++++++++++
src/authz.c | 295 +++++
src/cache/.gitignore | 4 +
src/cache/cache.h | 75 ++
src/cache/file.c | 474 +++++++
src/cache/memcache.c | 259 ++++
src/cache/shm.c | 350 ++++++
src/config.c | 1093 +++++++++++++++++
src/crypto.c | 366 ++++++
src/metadata.c | 999 +++++++++++++++
src/mod_auth_openidc.c | 1287 ++++++++++++++++++++
src/mod_auth_openidc.h | 316 +++++
src/oauth.c | 246 ++++
src/proto.c | 938 ++++++++++++++
src/session.c | 556 +++++++++
src/util.c | 1024 ++++++++++++++++
test/.gitignore | 1 +
test/mod_auth_openidc.jmx | 788 ++++++++++++
test/users.txt | 3 +
40 files changed, 10783 insertions(+)
create mode 100644 .gitignore
create mode 100644 ChangeLog
create mode 100644 LICENSE.txt
create mode 100644 Makefile.in
create mode 100644 README
create mode 100755 autogen.sh
create mode 100644 configure.ac
create mode 100644 debian/auth_openidc.conf
create mode 100644 debian/auth_openidc.load
create mode 100644 debian/changelog
create mode 100644 debian/compat
create mode 100644 debian/control
create mode 100644 debian/copyright
create mode 100644 debian/dirs
create mode 100644 debian/docs
create mode 100644 debian/libapache2-mod-auth-openidc.apache2
create mode 100644 debian/lintian-overrides
create mode 100755 debian/rules
create mode 100644 debian/source/format
create mode 100644 src/.gitignore
create mode 100644 src/apr_json.h
create mode 100644 src/apr_json_decode.c
create mode 100644 src/authz.c
create mode 100644 src/cache/.gitignore
create mode 100644 src/cache/cache.h
create mode 100644 src/cache/file.c
create mode 100644 src/cache/memcache.c
create mode 100644 src/cache/shm.c
create mode 100644 src/config.c
create mode 100644 src/crypto.c
create mode 100644 src/metadata.c
create mode 100644 src/mod_auth_openidc.c
create mode 100644 src/mod_auth_openidc.h
create mode 100644 src/oauth.c
create mode 100644 src/proto.c
create mode 100644 src/session.c
create mode 100644 src/util.c
create mode 100644 test/.gitignore
create mode 100644 test/mod_auth_openidc.jmx
create mode 100644 test/users.txt
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"
+ "