├── AUTHORS ├── NEWS ├── README ├── tests ├── requirements.txt ├── jwt.htpasswd ├── redhat_tests.sh ├── debian_tests.sh ├── test_jwt.py ├── apache_jwt.conf ├── apache_jwt_.conf ├── test_login.py └── test_auth_by_token.py ├── ChangeLog ├── COPYING ├── .gitignore ├── .travis.yml ├── Makefile.am ├── docker └── Dockerfile ├── acinclude.d └── ax_with_apxs.m4 ├── configure.ac ├── README.md ├── INSTALL └── mod_authnz_jwt.c /AUTHORS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | PyJWT 3 | cryptography 4 | -------------------------------------------------------------------------------- /tests/jwt.htpasswd: -------------------------------------------------------------------------------- 1 | test:$apr1$R0AvpH1k$Gv9AIerEow00/Lycv26ZR0 2 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | v1.0 2 | - Added directives AuthJWTFormUsername, AuthJWTFormPassword, AuthJWTAttributeUsername 3 | - Delete AuthJWTSub and sub check 4 | - Add authorization based on claims (Require jwt-claim and Require jwt-claim-array) 5 | 6 | 7 | v0.2 8 | - Support for EC and RSA in JWT signature 9 | - Added directives JWTSignaturePublicKeyFile and JWTSignaturePrivateKeyFile 10 | - Renamed directive JWTSignatureSecret to JWTSignatureSharedSecret 11 | 12 | v0.1 13 | - Support for HMAC in JWT signature 14 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright 2017 Anthony Deroche 2 | 2017 Raphael Medaer 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.lo 3 | *.slo 4 | *.la 5 | .libs/* 6 | tests/__pycache__/* 7 | .idea 8 | 9 | .deps 10 | Makefile 11 | acinclude.d/* 12 | config.h 13 | config.log 14 | config.status 15 | libtool 16 | 17 | # http://www.gnu.org/software/automake 18 | 19 | Makefile.in 20 | /ar-lib 21 | /mdate-sh 22 | /py-compile 23 | /test-driver 24 | /ylwrap 25 | 26 | # http://www.gnu.org/software/autoconf 27 | 28 | /autom4te.cache 29 | /autoscan.log 30 | /autoscan-*.log 31 | /aclocal.m4 32 | /compile 33 | /config.guess 34 | /config.h.in 35 | /config.sub 36 | /configure 37 | /configure.scan 38 | /depcomp 39 | /install-sh 40 | /missing 41 | /stamp-h1 42 | 43 | # https://www.gnu.org/software/libtool/ 44 | 45 | /ltmain.sh 46 | 47 | # http://www.gnu.org/software/texinfo 48 | 49 | /texinfo.tex 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: python 4 | python: 5 | - "3.4" 6 | addons: 7 | apt: 8 | packages: 9 | - automake 10 | - libtool 11 | - pkg-config 12 | - autoconf 13 | - libssl-dev 14 | - check 15 | - libjansson-dev 16 | - git 17 | - apache2 18 | - apache2-dev 19 | before_install: 20 | - git clone https://github.com/benmcollins/libjwt 21 | - cd libjwt 22 | - git checkout tags/v1.12.0 -q 23 | - autoreconf -i 24 | - ./configure 25 | - make 26 | - sudo make install 27 | - sudo ldconfig 28 | - cd .. 29 | install: 30 | - autoreconf -ivf 31 | - ./configure 32 | - make 33 | - sudo make install 34 | before_script: 35 | - pip install pyjwt requests cryptography 36 | - chmod ugo+x tests/debian_tests.sh 37 | - cd tests 38 | script: ./debian_tests.sh 39 | 40 | -------------------------------------------------------------------------------- /tests/redhat_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | sudo cp apache_jwt.conf /etc/httpd/conf.d/ 4 | sudo cp jwt.htpasswd /var/www/jwt.htpasswd 5 | sudo mkdir -p /var/www/testjwt/ 6 | sudo touch /var/www/testjwt/index.html 7 | 8 | mkdir -p /opt/mod_jwt_tests 9 | 10 | sudo openssl ecparam -name secp521r1 -genkey -noout -out /opt/mod_jwt_tests/ec-priv.pem 11 | sudo openssl ec -in /opt/mod_jwt_tests/ec-priv.pem -pubout -out /opt/mod_jwt_tests/ec-pub.pem 12 | 13 | sudo openssl genpkey -algorithm RSA -out /opt/mod_jwt_tests/rsa-priv.pem -pkeyopt rsa_keygen_bits:4096 14 | sudo openssl rsa -pubout -in /opt/mod_jwt_tests/rsa-priv.pem -out /opt/mod_jwt_tests/rsa-pub.pem 15 | 16 | sudo systemctl restart httpd 17 | 18 | if ! grep -q "testjwt.local" /etc/hosts; then 19 | echo "127.0.0.1 testjwt.local" | sudo tee --append /etc/hosts > /dev/null 20 | fi 21 | 22 | python3 -m unittest discover . -v -f 23 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | ACLOCAL_AMFLAGS = -I acinclude.d 2 | 3 | noinst_LTLIBRARIES = libmodauthnzjwt.la 4 | noinst_DATA = mod_authnz_jwt.la 5 | 6 | AM_CPPFLAGS = ${APACHE_CFLAGS} ${JWT_CFLAGS} ${JANSSON_CFLAGS} ${Z_CFLAGS} 7 | AM_LDFLAGS = ${JWT_LIBS} ${JANSSON_LDFLAGS} ${Z_LIBS} ${APR_LDFLAGS} 8 | 9 | libmodauthnzjwt_la_SOURCES = mod_authnz_jwt.c 10 | 11 | AM_CXXFLAGS = -Wall 12 | 13 | if NITPICK 14 | AM_CXXFLAGS += -Wextra -Wundef -Wshadow -Wunsafe-loop-optimizations -Wconversion -Wmissing-format-attribute 15 | AM_CXXFLAGS += -Wredundant-decls -ansi -Wmissing-noreturn 16 | endif 17 | 18 | if DEBUG 19 | AM_CXXFLAGS += -DDEBUG 20 | endif 21 | 22 | install-exec-local: 23 | ${APXS} -i -a -n 'auth_jwt' mod_authnz_jwt.la 24 | 25 | mod_authnz_jwt.la: libmodauthnzjwt.la 26 | ${APXS} -c -o $@ $< ${APACHE_CFLAGS} ${JWT_CFLAGS} ${JWT_LIBS} \ 27 | ${JANSSON_CFLAGS} ${JANSSON_LDFLAGS} ${Z_CFLAGS} ${Z_LIBS} 28 | -------------------------------------------------------------------------------- /tests/debian_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | sudo cp apache_jwt.conf /etc/apache2/sites-available/ 4 | sudo cp jwt.htpasswd /var/www/jwt.htpasswd 5 | sudo mkdir -p /var/www/testjwt/ 6 | sudo touch /var/www/testjwt/index.html 7 | 8 | sudo mkdir -p /opt/mod_jwt_tests 9 | 10 | sudo openssl ecparam -name secp256k1 -genkey -noout -out /opt/mod_jwt_tests/ec-priv.pem 11 | sudo openssl ec -in /opt/mod_jwt_tests/ec-priv.pem -pubout -out /opt/mod_jwt_tests/ec-pub.pem 12 | 13 | sudo openssl genpkey -algorithm RSA -out /opt/mod_jwt_tests/rsa-priv.pem -pkeyopt rsa_keygen_bits:4096 14 | sudo openssl rsa -pubout -in /opt/mod_jwt_tests/rsa-priv.pem -out /opt/mod_jwt_tests/rsa-pub.pem 15 | 16 | sudo chmod 644 /opt/mod_jwt_tests/*.pem 17 | 18 | 19 | if ! sudo a2query -m rewrite > /dev/null; then 20 | sudo a2enmod rewrite 21 | fi 22 | if ! sudo a2query -s apache_jwt > /dev/null; then 23 | sudo a2ensite apache_jwt 24 | fi 25 | sudo service apache2 restart 26 | 27 | if ! grep -q "testjwt.local" /etc/hosts; then 28 | echo "127.0.0.1 testjwt.local" | sudo tee --append /etc/hosts > /dev/null 29 | fi 30 | 31 | python3 -m unittest discover . -v -f 32 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim as build 2 | 3 | WORKDIR /build 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y ca-certificates make automake git g++ libtool pkg-config autoconf libssl-dev check libjansson-dev libz-dev procps apache2 apache2-dev 7 | 8 | ARG LIBJWT_VERSION=1.12.1 9 | ARG MOD_AUTHNZ_JWT_VERSION=1.2.0 10 | 11 | RUN git clone https://github.com/benmcollins/libjwt.git && \ 12 | cd libjwt && \ 13 | git checkout tags/v$LIBJWT_VERSION && \ 14 | autoreconf -i && \ 15 | ./configure && \ 16 | make && \ 17 | make install 18 | 19 | RUN git clone https://github.com/AnthonyDeroche/mod_authnz_jwt.git && \ 20 | cd mod_authnz_jwt && \ 21 | git checkout tags/v$MOD_AUTHNZ_JWT_VERSION && \ 22 | autoreconf -ivf && \ 23 | PKG_CONFIG_PATH=/usr/local ./configure && \ 24 | make && \ 25 | make install 26 | 27 | FROM httpd:2.4 28 | 29 | COPY --from=build /usr/local/lib/libjwt.so /usr/lib/x86_64-linux-gnu/libjwt.so.1 30 | COPY --from=build /usr/lib/apache2/modules/mod_authnz_jwt.so /usr/local/apache2/modules/mod_authnz_jwt.so 31 | 32 | RUN echo "LoadModule auth_jwt_module modules/mod_authnz_jwt.so" >> /usr/local/apache2/conf/httpd.conf 33 | 34 | -------------------------------------------------------------------------------- /acinclude.d/ax_with_apxs.m4: -------------------------------------------------------------------------------- 1 | # some code taken from mod_python's (http://www.modpython.org/) configure.in 2 | 3 | AC_DEFUN([AX_WITH_APXS], 4 | [ 5 | 6 | # check for --with-apxs 7 | AC_MSG_CHECKING(for --with-apxs) 8 | AC_ARG_WITH(apxs, AC_HELP_STRING([--with-apxs=PATH], [Path to apxs]), 9 | [ 10 | if test -x "$withval" 11 | then 12 | AC_MSG_RESULT([$withval executable, good]) 13 | APXS=$withval 14 | else 15 | echo 16 | AC_MSG_ERROR([$withval not found or not executable]) 17 | fi 18 | ], 19 | AC_MSG_RESULT(no)) 20 | 21 | # find apxs 22 | if test -z "$APXS"; then 23 | AC_PATH_PROGS([APXS],[apxs2 apxs],[false],[${PATH}:/usr/local/bin:/usr/local/sbin:/usr/sbin:/sbin]) 24 | test "${APXS}" = "false" && AC_MSG_ERROR([failed to find apxs. Try using --with-apxs]) 25 | fi 26 | 27 | # check Apache version 28 | AC_MSG_CHECKING(Apache version) 29 | HTTPD="`${APXS} -q SBINDIR`/`${APXS} -q TARGET`" 30 | if test ! -x "$HTTPD"; then 31 | AC_MSG_ERROR($APXS says that your apache binary lives at $HTTPD but that file isn't executable. Specify the correct apxs location with --with-apxs) 32 | fi 33 | ver=`$HTTPD -v | /usr/bin/awk '/version/ {print $3}' | /usr/bin/awk -F/ '{print $2}'` 34 | AC_MSG_RESULT($ver) 35 | 36 | # make sure version begins with 2 37 | if test -z "`$HTTPD -v | egrep 'Server version: Apache/2'`"; then 38 | AC_MSG_ERROR([mod_auth_openid only works with Apache 2. The one you have seems to be $ver.]) 39 | fi 40 | 41 | AC_SUBST(APXS) 42 | ]) 43 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_INIT([mod_authnz_jwt], [0.8], [anthony@deroche.me]) 2 | AM_CONFIG_HEADER(config.h) 3 | AM_INIT_AUTOMAKE() 4 | 5 | AC_CONFIG_MACRO_DIR([acinclude.d]) 6 | 7 | AC_PROG_CXX 8 | AC_PROG_CXXCPP 9 | AC_LANG_CPLUSPLUS 10 | AC_CANONICAL_HOST 11 | AC_PROG_INSTALL 12 | AM_PROG_LIBTOOL 13 | 14 | AC_HEADER_STDC 15 | 16 | # provide flag --enable-debug 17 | AC_ARG_ENABLE([debug], 18 | [ --enable-debug Enable debugging output to Apache error log], 19 | [case "${enableval}" in 20 | yes) debug=true ;; 21 | no) debug=false ;; 22 | *) AC_MSG_ERROR([bad value ${enableval} for --enable-debug]) ;; 23 | esac],[debug=false]) 24 | AM_CONDITIONAL([DEBUG], [test x$debug = xtrue]) 25 | 26 | # this will look for apxs command - put it in $APXS, fail on failure 27 | AX_WITH_APXS() 28 | 29 | # find apr-config binary 30 | AC_ARG_WITH(apr_config, AC_HELP_STRING([[--with-apr-config=FILE]], [Path to apr-config program]), 31 | [ apr_config="$withval" ], 32 | [AC_PATH_PROGS(apr_config, 33 | [apr-config apr-0-config apr-1-config], 34 | [no], 35 | [$PATH:/usr/sbin/:/usr/local/apache2/bin] 36 | )] 37 | ) 38 | 39 | if test "$apr_config" = "no"; then 40 | AC_MSG_ERROR(Could not find the apr-config program. You can specify a location with the --with-apr-config=FILE option. It may be named apr-0-config or apr-1-config and can be found in your apache2 bin directory.) 41 | fi 42 | 43 | $apr_config --cppflags &> /dev/null 44 | if test "$?" != "0"; then 45 | AC_MSG_ERROR($apr_config is not a valid apr-config program) 46 | fi 47 | 48 | APR_LDFLAGS="`${apr_config} --link-ld --libs`" 49 | AC_SUBST(APR_LDFLAGS) 50 | 51 | APACHE_CFLAGS="-I`${APXS} -q INCLUDEDIR` -I`${apr_config} --includedir`" 52 | AC_SUBST(APACHE_CFLAGS) 53 | 54 | PKG_CHECK_MODULES([JWT], [libjwt >= 1.7]) 55 | PKG_CHECK_MODULES([JANSSON], [jansson >= 2.0]) 56 | PKG_CHECK_MODULES([Z], [zlib]) 57 | 58 | # Idea taken from libopekele 59 | nitpick=false 60 | AC_ARG_ENABLE([nitpicking], 61 | AC_HELP_STRING([--enable-nitpicking],[make compiler warn about possible problems]), 62 | [ test "$enableval" = "no" || nitpick=true ] 63 | ) 64 | AM_CONDITIONAL(NITPICK, test x$nitpick = xtrue) 65 | 66 | AC_CONFIG_FILES([ 67 | Makefile 68 | ]) 69 | AC_OUTPUT 70 | 71 | -------------------------------------------------------------------------------- /tests/test_jwt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import requests 3 | import jwt 4 | import json 5 | import sys 6 | from contextlib import contextmanager 7 | from functools import wraps 8 | import base64 9 | 10 | 11 | class TestJWT(unittest.TestCase): 12 | 13 | BASE_URL = "http://testjwt.local/" 14 | 15 | USERNAME = "test" 16 | PASSWORD = "test" 17 | HMAC_SHARED_SECRET_BASE64 = "bnVsbGNoYXIAc2VjcmV0" 18 | USERNAME_ATTRIBUTE = "user" 19 | USERNAME_FIELD = "user" 20 | PASSWORD_FIELD = "password" 21 | JWT_EXPDELAY = 1800 22 | JWT_NBF_DELAY = 0 23 | JWT_ISS = "testjwt.local" 24 | JWT_AUD = "tests" 25 | JWT_LEEWAY = 10 26 | ALGORITHMS = ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"] 27 | 28 | @classmethod 29 | def with_all_algorithms(cls, algorithms=None, delivery="json"): 30 | if algorithms is None: 31 | algorithms = cls.ALGORITHMS 32 | def decorator(func): 33 | @wraps(func) 34 | def handler(_self): 35 | for alg in algorithms: 36 | baseUrl = cls.BASE_URL + alg + '/' + delivery; 37 | secured_url = baseUrl + '/secured' 38 | login_url = baseUrl + '/login' 39 | if alg in ("HS256", "HS384", "HS512"): 40 | private_key = base64.b64decode(cls.HMAC_SHARED_SECRET_BASE64) 41 | public_key = private_key 42 | elif alg in ("RS256", "RS384", "RS512"): 43 | f_priv = open("/opt/mod_jwt_tests/rsa-priv.pem") 44 | private_key = f_priv.read() 45 | f_priv.close() 46 | f_pub = open("/opt/mod_jwt_tests/rsa-pub.pem") 47 | public_key = f_pub.read() 48 | f_pub.close() 49 | elif alg in ("ES256", "ES384", "ES512"): 50 | f_priv = open("/opt/mod_jwt_tests/ec-priv.pem") 51 | private_key = f_priv.read() 52 | f_priv.close() 53 | f_pub = open("/opt/mod_jwt_tests/ec-pub.pem") 54 | public_key = f_pub.read() 55 | f_pub.close() 56 | with _self.subTest(alg=alg, public_key=public_key, private_key=private_key): 57 | func(_self, alg, public_key, private_key, secured_url, login_url) 58 | return handler 59 | return decorator 60 | 61 | def setUp(self): 62 | pass 63 | 64 | def tearDown(self): 65 | pass 66 | 67 | def http_get(self, url, token=None): 68 | headers = {} 69 | if token is not None: 70 | headers = {"Authorization": "Bearer %s" % token} 71 | r = requests.get(url, headers=headers) 72 | return r.status_code, r.content.decode('utf-8'), r.headers 73 | 74 | def http_post(self, url, data, token=None, headers=None): 75 | if headers is None: 76 | headers = {} 77 | if "Content-Type" not in headers: 78 | headers["Content-Type"] = "application/x-www-form-urlencoded" 79 | if "Authorization" not in headers and token is not None: 80 | headers["Authorization"] = "Bearer %s" % token 81 | r = requests.post(url, data=data, headers=headers) 82 | return r.status_code, r.content.decode('utf-8'), r.headers, r.cookies 83 | 84 | def decode_jwt(self, token, key, algorithm): 85 | return jwt.decode(token, key, audience="tests", algorithms=[algorithm]) 86 | 87 | def encode_jwt(self, payload, key, algorithm): 88 | return jwt.encode(payload, key, algorithm=algorithm).decode('utf-8') 89 | -------------------------------------------------------------------------------- /tests/apache_jwt.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName testjwt.local 3 | DocumentRoot /var/www/testjwt/ 4 | 5 | AuthJWTExpDelay 1800 6 | AuthJWTNbfDelay 0 7 | AuthJWTIss testjwt.local 8 | AuthJWTAud tests 9 | AuthJWTLeeway 10 10 | 11 | LogLevel auth_jwt:trace8 12 | RewriteEngine On 13 | 14 | 15 | AllowOverride None 16 | Options -Indexes 17 | Require all granted 18 | 19 | 20 | # first segment define algorithm 21 | 22 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 23 | AuthJWTSignatureAlgorithm HS256 24 | 25 | 26 | 27 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 28 | AuthJWTSignatureAlgorithm HS384 29 | 30 | 31 | 32 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 33 | AuthJWTSignatureAlgorithm HS512 34 | 35 | 36 | 37 | 38 | AuthJWTSignatureAlgorithm RS256 39 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/rsa-pub.pem 40 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/rsa-priv.pem 41 | 42 | 43 | 44 | AuthJWTSignatureAlgorithm RS384 45 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/rsa-pub.pem 46 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/rsa-priv.pem 47 | 48 | 49 | 50 | AuthJWTSignatureAlgorithm RS512 51 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/rsa-pub.pem 52 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/rsa-priv.pem 53 | 54 | 55 | 56 | AuthJWTSignatureAlgorithm ES256 57 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/ec-pub.pem 58 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/ec-priv.pem 59 | 60 | 61 | 62 | AuthJWTSignatureAlgorithm ES384 63 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/ec-pub.pem 64 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/ec-priv.pem 65 | 66 | 67 | 68 | AuthJWTSignatureAlgorithm ES512 69 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/ec-pub.pem 70 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/ec-priv.pem 71 | 72 | 73 | # second segment define delivery type/auth type 74 | 75 | AuthJWTDeliveryType Cookie 76 | AuthType jwt-cookie 77 | 78 | 79 | 80 | AuthJWTDeliveryType JSON 81 | AuthType jwt-json 82 | 83 | 84 | 85 | AuthJWTDeliveryType JSON 86 | AuthType jwt-both 87 | 88 | 89 | # last segment is either the login or access to protected/secured resources 90 | 91 | SetHandler jwt-login-handler 92 | AuthJWTProvider file 93 | AuthUserFile /var/www/jwt.htpasswd 94 | 95 | 96 | AliasMatch "secured" "/var/www/testjwt/index.html" 97 | 98 | AuthType jwt-bearer 99 | 100 | AllowOverride None 101 | Options -Indexes 102 | AuthName "private area" 103 | Require valid-user 104 | 105 | 106 | #special cases 107 | 108 | AuthJWTTokenName CustomToken 109 | 110 | 111 | 112 | AuthJWTCookieName CustomCookie 113 | 114 | 115 | 116 | AuthJWTCookieAttr path=/secure;CustomAttr 117 | 118 | 119 | -------------------------------------------------------------------------------- /tests/apache_jwt_.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName testjwt.local 3 | DocumentRoot /var/www/testjwt/ 4 | 5 | AuthJWTExpDelay 1800 6 | AuthJWTNbfDelay 0 7 | AuthJWTIss testjwt.local 8 | AuthJWTAud tests 9 | AuthJWTLeeway 10 10 | 11 | LogLevel auth_jwt:debug 12 | 13 | #AliasMatch secured$ /var/www/testjwt/index.html 14 | 15 | 16 | AllowOverride None 17 | Options -Indexes 18 | Require all granted 19 | 20 | 21 | Alias "/HS256/secured" "/var/www/testjwt" 22 | 23 | 24 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 25 | AuthJWTSignatureAlgorithm HS256 26 | AllowOverride None 27 | Options -Indexes 28 | AuthType jwt 29 | AuthName "private area" 30 | Require valid-user 31 | 32 | 33 | # first segment define algorithm 34 | # 35 | # AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 36 | # AuthJWTSignatureAlgorithm HS256 37 | # 38 | 39 | 40 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 41 | AuthJWTSignatureAlgorithm HS384 42 | 43 | 44 | 45 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 46 | AuthJWTSignatureAlgorithm HS512 47 | 48 | 49 | 50 | 51 | AuthJWTSignatureAlgorithm RS256 52 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/rsa-pub.pem 53 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/rsa-priv.pem 54 | 55 | 56 | 57 | AuthJWTSignatureAlgorithm RS384 58 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/rsa-pub.pem 59 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/rsa-priv.pem 60 | 61 | 62 | 63 | AuthJWTSignatureAlgorithm RS512 64 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/rsa-pub.pem 65 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/rsa-priv.pem 66 | 67 | 68 | 69 | AuthJWTSignatureAlgorithm ES256 70 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/ec-pub.pem 71 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/ec-priv.pem 72 | 73 | 74 | 75 | AuthJWTSignatureAlgorithm ES384 76 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/ec-pub.pem 77 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/ec-priv.pem 78 | 79 | 80 | 81 | AuthJWTSignatureAlgorithm ES512 82 | AuthJWTSignaturePublicKeyFile /opt/mod_jwt_tests/ec-pub.pem 83 | AuthJWTSignaturePrivateKeyFile /opt/mod_jwt_tests/ec-priv.pem 84 | 85 | 86 | # second segment define delivery type/auth type 87 | 88 | AuthJWTDeliveryType Cookie 89 | AuthType jwt-cookie 90 | 91 | 92 | 93 | AuthJWTDeliveryType JSON 94 | AuthType jwt-json 95 | 96 | 97 | 98 | AuthJWTDeliveryType JSON 99 | AuthType jwt 100 | 101 | 102 | # end segment define login or access 103 | 104 | SetHandler jwt-login-handler 105 | AuthJWTProvider file 106 | AuthUserFile /var/www/jwt.htpasswd 107 | 108 | 109 | # 110 | # AuthType jwt 111 | 112 | # AllowOverride None 113 | # Options -Indexes 114 | # AuthName "private area" 115 | # Require valid-user 116 | # 117 | 118 | 119 | AuthJWTDeliveryType Cookie 120 | AuthJWTCookieName CustomCookie 121 | AuthJWTSignatureAlgorithm HS256 122 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 123 | SetHandler jwt-login-handler 124 | AuthJWTProvider file 125 | AuthUserFile /var/www/jwt.htpasswd 126 | 127 | 128 | 129 | AuthJWTDeliveryType Cookie 130 | AuthJWTCookieAttr path=/secure;CustomAttr 131 | AuthJWTSignatureAlgorithm HS256 132 | AuthJWTSignatureSharedSecret bnVsbGNoYXIAc2VjcmV0 133 | SetHandler jwt-login-handler 134 | AuthJWTProvider file 135 | AuthUserFile /var/www/jwt.htpasswd 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /tests/test_login.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | import json 4 | from test_jwt import TestJWT 5 | 6 | 7 | class TestLogin(TestJWT): 8 | 9 | def assertToken(self, token, public_key, alg): 10 | jwt_fields = self.decode_jwt(token, public_key, alg) 11 | self.assertTrue(all(claim in jwt_fields for claim in [self.USERNAME_ATTRIBUTE, "exp", "nbf", "iat", "iss", "aud"])) 12 | self.assertEqual(jwt_fields[self.USERNAME_ATTRIBUTE], self.USERNAME) 13 | # we assume this test takes less than 1s 14 | self.assertTrue(int(jwt_fields["iat"]) - int(time.time())<1) 15 | self.assertEqual(int(jwt_fields["exp"])-int(jwt_fields["iat"]), self.JWT_EXPDELAY) 16 | self.assertEqual(jwt_fields["iss"], self.JWT_ISS) 17 | self.assertEqual(int(jwt_fields["nbf"]), int(jwt_fields["iat"])+self.JWT_NBF_DELAY) 18 | 19 | @TestJWT.with_all_algorithms(algorithms=("HS256", "RS256", "ES256")) 20 | def test_login_should_success_with_json(self, alg, public_key, private_key, secured_url, login_url): 21 | code, content, headers, cookies = self.http_post(login_url, {self.USERNAME_FIELD:self.USERNAME, self.PASSWORD_FIELD:self.PASSWORD}) 22 | 23 | # we expect return code 200, JSON content type 24 | self.assertEqual(code, 200) 25 | self.assertEqual(headers.get("Content-Type"), "application/json") 26 | 27 | # we check if the JSON object is correct and token is valid 28 | received_object = json.loads(content) 29 | self.assertTrue("token" in received_object) 30 | 31 | self.assertToken(received_object["token"], public_key, alg) 32 | 33 | @TestJWT.with_all_algorithms(algorithms=("HS256", "RS256", "ES256")) 34 | def test_login_should_success_with_custom_token_name(self, alg, public_key, private_key, secured_url, login_url): 35 | code, content, headers, cookies = self.http_post(login_url + "/token_custom_name", {self.USERNAME_FIELD:self.USERNAME, self.PASSWORD_FIELD:self.PASSWORD}) 36 | 37 | # we expect return code 200, JSON content type 38 | self.assertEqual(code, 200) 39 | self.assertEqual(headers.get("Content-Type"), "application/json") 40 | 41 | # we check if the JSON object is correct and token is valid 42 | received_object = json.loads(content) 43 | self.assertTrue("CustomToken" in received_object) 44 | 45 | self.assertToken(received_object["CustomToken"], public_key, alg) 46 | 47 | @TestJWT.with_all_algorithms(algorithms=("HS256", "RS256", "ES256")) 48 | def test_login_with_bad_credentials_should_fail(self, alg, public_key, private_key, secured_url, login_url): 49 | code, content, headers, cookies = self.http_post(login_url, {self.USERNAME_FIELD:self.USERNAME, self.PASSWORD_FIELD:"azerty"}) 50 | self.assertEqual(code, 401) 51 | code, content, headers, cookies = self.http_post(login_url, {self.USERNAME_FIELD:self.USERNAME, self.PASSWORD_FIELD:""}) 52 | self.assertEqual(code, 401) 53 | code, content, headers, cookies = self.http_post(login_url, {self.USERNAME_FIELD:self.USERNAME}) 54 | self.assertEqual(code, 401) 55 | code, content, headers, cookies = self.http_post(login_url, {}) 56 | self.assertEqual(code, 401) 57 | 58 | @TestJWT.with_all_algorithms(algorithms=("HS256", "RS256", "ES256")) 59 | def test_get_on_login_path_should_fail(self, alg, public_key, private_key, secured_url, login_url): 60 | code, content, headers = self.http_get(login_url) 61 | self.assertEqual(code, 405) 62 | 63 | # Cookie delivery tests # 64 | 65 | @TestJWT.with_all_algorithms(algorithms=("HS256", "RS256", "ES256"), delivery="cookie") 66 | def test_login_should_success_with_cookie(self, alg, public_key, private_key, secured_url, login_url): 67 | code, content, headers, cookies = self.http_post(login_url, {self.USERNAME_FIELD:self.USERNAME, self.PASSWORD_FIELD:self.PASSWORD}) 68 | 69 | self.assertEqual(code, 200) 70 | 71 | self.assertTrue("AuthToken" in cookies) 72 | self.assertToken(cookies["AuthToken"], public_key, alg) 73 | 74 | # TODO add test when using bad cookie name 75 | @TestJWT.with_all_algorithms(algorithms=("HS256",), delivery="cookie") 76 | def test_login_should_success_with_custom_cookie_name(self, alg, public_key, private_key, secured_url, login_url): 77 | code, content, headers, cookies = self.http_post(login_url + "/cookie_custom_name", {self.USERNAME_FIELD:self.USERNAME, self.PASSWORD_FIELD:self.PASSWORD}) 78 | 79 | self.assertEqual(code, 200) 80 | 81 | self.assertEqual(len(cookies), 1) 82 | 83 | # cookies.get("AuthToken") will return cookie.value not the cookie object 84 | for c in cookies: 85 | self.assertEqual(c.name, "CustomCookie") 86 | self.assertTrue(c.secure) 87 | self.assertTrue(c.has_nonstandard_attr("HttpOnly")) 88 | self.assertTrue(c.has_nonstandard_attr("SameSite")) 89 | 90 | self.assertToken(c.value, public_key, alg) 91 | 92 | @TestJWT.with_all_algorithms(algorithms=("HS256",), delivery="cookie") 93 | def test_login_should_success_with_custom_cookie_attributes(self, alg, public_key, private_key, secured_url, login_url): 94 | code, content, headers, cookies = self.http_post(login_url + "/cookie_custom_attr", {self.USERNAME_FIELD:self.USERNAME, self.PASSWORD_FIELD:self.PASSWORD}) 95 | 96 | self.assertEqual(code, 200) 97 | 98 | self.assertEqual(len(cookies), 1) 99 | 100 | for c in cookies: 101 | self.assertEqual(c.name, "AuthToken") 102 | self.assertTrue(c.path, "/secure") 103 | self.assertTrue(c.has_nonstandard_attr('CustomAttr')) 104 | 105 | self.assertToken(c.value, public_key, alg) 106 | -------------------------------------------------------------------------------- /tests/test_auth_by_token.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | import json 4 | from test_jwt import TestJWT 5 | 6 | 7 | class TestAuthByToken(TestJWT): 8 | 9 | @TestJWT.with_all_algorithms() 10 | def test_login_with_urlencoded_should_success(self, alg, public_key, private_key, secured_url, login_url): 11 | code, content, headers, cookies = self.http_post(login_url, { 12 | self.USERNAME_FIELD: self.USERNAME, 13 | self.PASSWORD_FIELD: self.PASSWORD 14 | }, 15 | headers={"Content-Type": "application/x-www-form-urlencoded"}) 16 | self.assertEqual(code, 200) 17 | 18 | @TestJWT.with_all_algorithms() 19 | def test_login_with_json_should_fail(self, alg, public_key, private_key, secured_url, login_url): 20 | code, content, headers, cookies = self.http_post(login_url, { 21 | self.USERNAME_FIELD: self.USERNAME, 22 | self.PASSWORD_FIELD: self.PASSWORD 23 | }, 24 | headers={"Content-Type": "application/json"}) 25 | self.assertEqual(code, 415) 26 | 27 | @TestJWT.with_all_algorithms() 28 | def test_malformed_token_should_fail(self, alg, public_key, private_key, secured_url, login_url): 29 | token = self.encode_jwt( 30 | {"iss": self.JWT_ISS, "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()), "nbf": int(time.time()), 31 | "exp": int(time.time()) + 1000}, private_key, alg) 32 | # we replace . by # for the token to be malformed 33 | token = token.replace('.', '#') 34 | code, content, headers = self.http_get(secured_url, token=token) 35 | self.assertEqual(code, 401) 36 | self.assertEqual(headers["WWW-Authenticate"], 37 | 'Bearer realm="private area", error="invalid_token", ' 38 | 'error_description="Token is malformed or signature is invalid"') 39 | 40 | @TestJWT.with_all_algorithms() 41 | def test_invalid_signature_should_fail(self, alg, public_key, private_key, secured_url, login_url): 42 | token = self.encode_jwt( 43 | {"iss": self.JWT_ISS, "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()), "nbf": int(time.time()), 44 | "exp": int(time.time()) + 1000}, private_key, alg) 45 | # we remove last 10 characters for the signature to be invalid 46 | token = token[:-10] 47 | code, content, headers = self.http_get(secured_url, token=token) 48 | self.assertEqual(code, 401) 49 | self.assertEqual(headers["WWW-Authenticate"], 50 | 'Bearer realm="private area", error="invalid_token", ' 51 | 'error_description="Token is malformed or signature is invalid"') 52 | 53 | @TestJWT.with_all_algorithms() 54 | def test_invalid_iss_should_fail(self, alg, public_key, private_key, secured_url, login_url): 55 | token = self.encode_jwt( 56 | {"iss": "invalid", "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()), "nbf": int(time.time()), 57 | "exp": int(time.time()) + 1000}, private_key, alg) 58 | code, content, headers = self.http_get(secured_url, token=token) 59 | self.assertEqual(code, 401) 60 | self.assertEqual(headers["WWW-Authenticate"], 61 | 'Bearer realm="private area", error="invalid_token", ' 62 | 'error_description="Issuer is not valid"') 63 | 64 | @TestJWT.with_all_algorithms() 65 | def test_invalid_aud_should_fail(self, alg, public_key, private_key, secured_url, login_url): 66 | token = self.encode_jwt( 67 | {"iss": self.JWT_ISS, "aud": "invalid", "user": "toto", "iat": int(time.time()), "nbf": int(time.time()), 68 | "exp": int(time.time()) + 1000}, private_key, alg) 69 | code, content, headers = self.http_get(secured_url, token=token) 70 | self.assertEqual(code, 401) 71 | self.assertEqual(headers["WWW-Authenticate"], 72 | 'Bearer realm="private area", error="invalid_token", ' 73 | 'error_description="Audience is not valid"') 74 | 75 | @TestJWT.with_all_algorithms() 76 | def test_invalid_nbf_should_fail(self, alg, public_key, private_key, secured_url, login_url): 77 | token = self.encode_jwt({"iss": self.JWT_ISS, "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()), 78 | "nbf": int(time.time()) + 1000, "exp": int(time.time()) + 1000}, private_key, alg) 79 | code, content, headers = self.http_get(secured_url, token=token) 80 | self.assertEqual(code, 401) 81 | self.assertEqual(headers["WWW-Authenticate"], 82 | 'Bearer realm="private area", error="invalid_token", ' 83 | 'error_description="Token can\'t be processed now due to nbf field"') 84 | 85 | @TestJWT.with_all_algorithms() 86 | def test_invalid_exp_should_fail(self, alg, public_key, private_key, secured_url, login_url): 87 | token = self.encode_jwt( 88 | {"iss": self.JWT_ISS, "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()), "nbf": int(time.time()), 89 | "exp": int(time.time()) - 1000}, private_key, alg) 90 | code, content, headers = self.http_get(secured_url, token=token) 91 | self.assertEqual(code, 401) 92 | self.assertEqual(headers["WWW-Authenticate"], 93 | 'Bearer realm="private area", error="invalid_token", error_description="Token expired"') 94 | 95 | @TestJWT.with_all_algorithms() 96 | def test_token_exp_missing_should_success(self, alg, public_key, private_key, secured_url, login_url): 97 | token = self.encode_jwt({"iss": self.JWT_ISS, "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()), 98 | "nbf": int(time.time())}, private_key, alg) 99 | code, content, headers = self.http_get(secured_url, token=token) 100 | self.assertEqual(code, 200) 101 | 102 | @TestJWT.with_all_algorithms() 103 | def test_with_leeway_should_success(self, alg, public_key, private_key, secured_url, login_url): 104 | token = self.encode_jwt( 105 | {"iss": self.JWT_ISS, "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()) - 1000, 106 | "nbf": int(time.time()) - 1000, "exp": int(time.time()) - self.JWT_LEEWAY + 1}, private_key, alg) 107 | code, content, headers = self.http_get(secured_url, token=token) 108 | self.assertEqual(code, 200) 109 | 110 | @TestJWT.with_all_algorithms() 111 | def test_should_success(self, alg, public_key, private_key, secured_url, login_url): 112 | token = self.encode_jwt( 113 | {"iss": self.JWT_ISS, "aud": self.JWT_AUD, "user": "toto", "iat": int(time.time()) - 1000, 114 | "nbf": int(time.time()) - 1000, "exp": int(time.time()) + 10}, private_key, alg) 115 | code, content, headers = self.http_get(secured_url, token=token) 116 | self.assertEqual(code, 200) 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mod_authnz_jwt 2 | 3 | Authentication module for Apache httpd with JSON web tokens (JWT). 4 | 5 | [![Build Status](https://travis-ci.org/AnthonyDeroche/mod_authnz_jwt.svg?branch=master)](https://travis-ci.org/AnthonyDeroche/mod_authnz_jwt) 6 | 7 | More on JWT : https://jwt.io/ 8 | 9 | Supported algorithms : HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512 10 | 11 | Built-in checks : iss, aud, exp, nbf 12 | 13 | Configurable checks : every claims contained in the token (only string and array) 14 | 15 | This module is able to deliver JSON web tokens containing all public fields (iss, aud, sub, iat, nbf, exp), and the private field "user". Authentication process is carried out by an authentication provider and specified by the AuthJWTProvider directive. 16 | 17 | On the other hand, this module is able to check validity of token based on its signature, and on its public fields. If the token is valid, then the user is authenticated and can be used by an authorization provider with the directive "Require valid-user" to authorize or not the request. 18 | 19 | Although this module is able to deliver valid tokens, it may be used to check tokens delivered by a custom application in any language, as long as a secret is shared between the two parts. This feature is possible because token-based authentication is stateless. 20 | 21 | ## Build Requirements 22 | 23 | - libjwt (https://github.com/benmcollins/libjwt) 24 | - Apache development package (apache2-dev on Debian/Ubuntu and httpd-devel on CentOS/Fedora) 25 | 26 | ## Quick start 27 | 28 | 29 | ### Installation using Docker 30 | 31 | See [Dockerfile](https://github.com/AnthonyDeroche/mod_authnz_jwt/blob/master/docker/Dockerfile) 32 | 33 | 34 | ### Installation from sources 35 | ~~~~ 36 | sudo apt-get install libtool pkg-config autoconf libssl-dev check libjansson-dev 37 | git clone https://github.com/benmcollins/libjwt 38 | cd libjwt 39 | git checkout tags/v1.12.1 40 | autoreconf -i 41 | ./configure 42 | make 43 | sudo make install 44 | cd .. 45 | sudo apt-get install apache2 apache2-dev libz-dev 46 | git clone https://github.com/AnthonyDeroche/mod_authnz_jwt 47 | cd mod_authnz_jwt 48 | autoreconf -ivf 49 | ./configure 50 | make 51 | sudo make install 52 | ~~~~ 53 | 54 | ### Generate EC keys 55 | ~~~~ 56 | openssl ecparam -name secp256k1 -genkey -noout -out ec-priv.pem 57 | openssl ec -in ec-priv.pem -pubout -out ec-pub.pem 58 | ~~~~ 59 | 60 | ### Generate RSA keys 61 | ~~~~ 62 | openssl genpkey -algorithm RSA -out rsa-priv.pem -pkeyopt rsa_keygen_bits:4096 63 | openssl rsa -pubout -in rsa-priv.pem -out rsa-pub.pem 64 | ~~~~ 65 | 66 | ### Authentication 67 | 68 | The common workflow is to authenticate against a token service using for instance username/password. Then we reuse this token to authenticate our next requests as long as the token remains valid. 69 | 70 | #### Using username/password 71 | 72 | You can configure the module to deliver a JWT if your username/password is correct. Use "AuthJWTProvider" to configure which providers will be used to authenticate the user. 73 | 74 | Authentication modules are for instance: 75 | - mod_authn_file (https://httpd.apache.org/docs/2.4/mod/mod_authn_file.html) 76 | - mod_authn_dbd (https://httpd.apache.org/docs/2.4/mod/mod_authn_dbd.html) 77 | - mod_authn_dbm (https://httpd.apache.org/docs/2.4/mod/mod_authn_dbm.html) 78 | - mod_authn_socache (https://httpd.apache.org/docs/2.4/mod/mod_authn_socache.html) 79 | - mod_authnz_ldap (https://httpd.apache.org/docs/2.4/mod/mod_authnz_ldap.html) 80 | - mod_authnz_fcgi (https://httpd.apache.org/docs/2.4/mod/mod_authnz_fcgi.html) 81 | - mod_authnz_external (https://code.google.com/archive/p/mod-auth-external/) 82 | - mod_authn_anon (https://httpd.apache.org/docs/2.4/mod/mod_authn_anon.html) 83 | 84 | The delivered token will contain your username in a field named "user" (See AuthJWTAttributeUsername to override this value) as well as public fields exp, iat, nbf and possibly iss and aud according to the configuration. 85 | 86 | A minimal configuration might be: 87 | ~~~~ 88 | AuthJWTSignatureAlgorithm HS256 89 | AuthJWTSignatureSharedSecret Q0hBTkdFTUU= 90 | AuthJWTIss example.com 91 | 92 | SetHandler jwt-login-handler 93 | AuthJWTProvider file 94 | AuthUserFile /var/www/jwt.htpasswd 95 | 96 | ~~~~ 97 | 98 | #### Using a JWT 99 | 100 | A secured area can be accessed if the provided JWT is valid. JWT must be set in Authorization header. Its value must be "Bearer ". 101 | 102 | If the signature is correct and fields are correct, then a secured location can be accessed. 103 | 104 | Token must not be expired (exp), not processed too early (nbf), and issuer/audience must match the configuration. 105 | 106 | A minimal configuration might be: 107 | ~~~~ 108 | AuthJWTSignatureAlgorithm HS256 109 | AuthJWTSignatureSharedSecret Q0hBTkdFTUU= 110 | AuthJWTIss example.com 111 | 112 | AllowOverride None 113 | AuthType jwt 114 | AuthName "private area" 115 | Require valid-user 116 | 117 | ~~~~ 118 | 119 | 120 | ### Authorization 121 | 122 | You can use the directive Require jwt-claim key1=value1 key2=value2. Putting multiple keys/values in the same require results in an OR. You can use RequireAny and RequireAll directives to be more precise in your rules. 123 | 124 | In case your key is an array, you can use the directive Require jwt-claim-array key1=value1 to test that "value1" is contained in the array pointed by the key "key1". 125 | 126 | Examples: 127 | ~~~~ 128 | AuthJWTSignatureAlgorithm HS256 129 | AuthJWTSignatureSharedSecret Q0hBTkdFTUU= 130 | AuthJWTIss example.com 131 | 132 | AllowOverride None 133 | AuthType jwt 134 | AuthName "private area" 135 | Require jwt-claim user=toto 136 | Require jwt-claim-array groups=group1 137 | 138 | ~~~~ 139 | 140 | ### How to get authenticated user in your apps? 141 | If your app is directly hosted by the same Apache than the module, then you can read the environment variable "REMOTE_USER". 142 | 143 | If the apache instance on which the module is installed acts as a reverse proxy, then you need to add a header in the request (X-Remote-User for example). We use mod_rewrite to do so. 144 | For your information, rewrite rules are interpreted before authentication. That's why why need a "look ahead" variable which will take its final value during the fixup phase. 145 | ~~~~ 146 | RewriteEngine On 147 | RewriteCond %{LA-U:REMOTE_USER} (.+) 148 | RewriteRule . - [E=RU:%1] 149 | RequestHeader set X-Remote-User "%{RU}e" env=RU 150 | ~~~~ 151 | ## Configuration examples 152 | 153 | This configuration is given for tests purpose. Remember to always use TLS in production. 154 | 155 | With HMAC algorithm: 156 | ~~~~ 157 | 158 | ServerName example.com 159 | DocumentRoot /var/www/html/ 160 | 161 | # default values 162 | AuthJWTFormUsername user 163 | AuthJWTFormPassword password 164 | AuthJWTAttributeUsername user 165 | 166 | AuthJWTSignatureAlgorithm HS256 167 | AuthJWTSignatureSharedSecret Q0hBTkdFTUU= 168 | AuthJWTExpDelay 1800 169 | AuthJWTNbfDelay 0 170 | AuthJWTIss example.com 171 | AuthJWTAud demo 172 | AuthJWTLeeway 10 173 | 174 | 175 | AllowOverride None 176 | AuthType jwt 177 | AuthName "private area" 178 | Require valid-user 179 | 180 | 181 | 182 | 183 | SetHandler jwt-login-handler 184 | AuthJWTProvider file 185 | AuthUserFile /var/www/jwt.htpasswd 186 | 187 | 188 | ErrorLog ${APACHE_LOG_DIR}/error.log 189 | CustomLog ${APACHE_LOG_DIR}/access.log combined 190 | 191 | ~~~~ 192 | 193 | With EC algorithm: 194 | ~~~~ 195 | 196 | ServerName example.com 197 | DocumentRoot /var/www/html/ 198 | 199 | # default values 200 | AuthJWTFormUsername user 201 | AuthJWTFormPassword password 202 | AuthJWTAttributeUsername user 203 | 204 | AuthJWTSignatureAlgorithm ES256 205 | AuthJWTSignaturePublicKeyFile /etc/pki/auth_pub.pem 206 | AuthJWTSignaturePrivateKeyFile /etc/pki/auth_priv.pem 207 | AuthJWTExpDelay 1800 208 | AuthJWTNbfDelay 0 209 | AuthJWTIss example.com 210 | AuthJWTAud demo 211 | AuthJWTLeeway 10 212 | 213 | 214 | AllowOverride None 215 | AuthType jwt 216 | AuthName "private area" 217 | Require valid-user 218 | 219 | 220 | 221 | 222 | SetHandler jwt-login-handler 223 | AuthJWTProvider file 224 | AuthUserFile /var/www/jwt.htpasswd 225 | 226 | 227 | ErrorLog ${APACHE_LOG_DIR}/error.log 228 | CustomLog ${APACHE_LOG_DIR}/access.log combined 229 | 230 | ~~~~ 231 | 232 | With Cookie: 233 | ~~~~ 234 | 235 | ServerName example.com 236 | DocumentRoot /var/www/html/ 237 | 238 | # default values 239 | AuthJWTFormUsername user 240 | AuthJWTFormPassword password 241 | AuthJWTAttributeUsername user 242 | 243 | AuthJWTSignatureAlgorithm HS256 244 | AuthJWTSignatureSharedSecret Q0hBTkdFTUU= 245 | AuthJWTExpDelay 1800 246 | AuthJWTNbfDelay 0 247 | AuthJWTIss example.com 248 | AuthJWTAud demo 249 | AuthJWTLeeway 10 250 | 251 | AuthJWTDeliveryType Cookie 252 | 253 | 254 | AllowOverride None 255 | AuthType jwt-cookie 256 | AuthName "private area" 257 | Require valid-user 258 | 259 | 260 | 261 | 262 | SetHandler jwt-login-handler 263 | AuthJWTProvider file 264 | AuthUserFile /var/www/jwt.htpasswd 265 | 266 | 267 | ErrorLog ${APACHE_LOG_DIR}/error.log 268 | CustomLog ${APACHE_LOG_DIR}/access.log combined 269 | 270 | ~~~~ 271 | 272 | ## Documentation 273 | 274 | #### Directives 275 | 276 | ##### AuthType 277 | * **Description**: Authentication type to allow. `jwt` and `jwt-bearer` will allow only the Authorization header. `jwt-cookie` allows only Cookie usage. `jwt-both` accepts Authorization header and cookie. Cookie value will be ignored if Authorization header is set. 278 | * **Context**: directory 279 | * **Possibles values**: jwt, jwt-bearer, jwt-cookie, jwt-both 280 | 281 | ##### AuthJWTProvider 282 | 283 | * **Description**: Authentication providers used 284 | * **Context**: directory 285 | 286 | ##### AuthJWTSignatureAlgorithm 287 | 288 | * **Description**: The algorithm to use to sign tokens 289 | * **Context**: server config, directory 290 | * **Default**: HS256 291 | * **Possibles values**: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512 292 | * **Mandatory**: yes 293 | 294 | ##### AuthJWTSignatureSharedSecret 295 | 296 | * **Description**: The secret to use to sign tokens with HMACs. It must be base64 encoded. 297 | * **Context**: server config, directory 298 | * **Mandatory**: no 299 | 300 | ##### AuthJWTSignaturePublicKeyFile 301 | 302 | * **Description**: The file path of public key used with either RSA or EC algorithms. 303 | * **Context**: server config, directory 304 | * **Mandatory**: no 305 | 306 | ##### AuthJWTSignaturePrivateKeyFile 307 | 308 | * **Description**: The file path of private key used with either RSA or EC algorithms. 309 | * **Context**: server config, directory 310 | * **Mandatory**: no 311 | 312 | ##### AuthJWTIss 313 | * **Description**: The issuer of delivered tokens 314 | * **Context**: server config, directory 315 | * **Mandatory**: no 316 | 317 | ##### AuthJWTAud 318 | * **Description**: The audience of delivered tokens 319 | * **Context**: server config, directory 320 | * **Mandatory**: no 321 | 322 | ##### AuthJWTExpDelay 323 | * **Description**: The time delay in seconds after which delivered tokens are considered invalid 324 | * **Context**: server config, directory 325 | * **Default**: 1800 326 | * **Mandatory**: no 327 | 328 | ##### AuthJWTNbfDelay 329 | * **Description**: The time delay in seconds before which delivered tokens must not be processed 330 | * **Context**: server config, directory 331 | * **Default**: 0 332 | * **Mandatory**: no 333 | 334 | ##### AuthJWTLeeway 335 | * **Description**: The leeway to account for clock skew in token validation process 336 | * **Context**: server config, directory 337 | * **Default**: 0 338 | * **Mandatory**: no 339 | 340 | ##### AuthJWTFormUsername 341 | * **Description**: The name of the field containing the username in authentication process 342 | * **Context**: server config, directory 343 | * **Default**: user 344 | * **Mandatory**: no 345 | 346 | ##### AuthJWTFormPassword 347 | * **Description**: The name of the field containing the password in authentication process 348 | * **Context**: server config, directory 349 | * **Default**: password 350 | * **Mandatory**: no 351 | 352 | ##### AuthJWTAttributeUsername 353 | * **Description**: The name of the attribute containing the username in the token (used for authorization as well as token generation) 354 | * **Context**: server config, directory 355 | * **Default**: user 356 | * **Mandatory**: no 357 | 358 | ##### AuthJWTDeliveryType 359 | * **Description**: Type of token delivery JSON or Cookie (case-sensitive) 360 | * **Context**: server config, directory 361 | * **Default**: JSON 362 | * **Possibles values**: JSON, Cookie 363 | * **Mandatory**: no 364 | 365 | ##### AuthJWTTokenName 366 | * **Description**: Token name to use when using JSON delivery 367 | * **Context**: server config, directory 368 | * **Default**: token 369 | * **Mandatory**: no 370 | 371 | ##### AuthJWTCookieName 372 | * **Description**: Cookie name to use when using cookie delivery 373 | * **Context**: server config, directory 374 | * **Default**: AuthToken 375 | * **Mandatory**: no 376 | 377 | ##### AuthJWTCookieAttr 378 | * **Description**: Semi-colon separated attributes for cookie when using cookie delivery 379 | * **Context**: server config, directory 380 | * **Default**: Secure;HttpOnly;SameSite 381 | * **Mandatory**: no 382 | 383 | ##### AuthJWTRemoveCookie 384 | * **Description**: Remove cookie from the headers, and thus keep it private from the backend 385 | * **Context**: server config, directory 386 | * **Default**: 1 387 | * **Mandatory**: no 388 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Installation Instructions 2 | ************************* 3 | 4 | Copyright (C) 1994-1996, 1999-2002, 2004-2016 Free Software 5 | Foundation, Inc. 6 | 7 | Copying and distribution of this file, with or without modification, 8 | are permitted in any medium without royalty provided the copyright 9 | notice and this notice are preserved. This file is offered as-is, 10 | without warranty of any kind. 11 | 12 | Basic Installation 13 | ================== 14 | 15 | Briefly, the shell command './configure && make && make install' 16 | should configure, build, and install this package. The following 17 | more-detailed instructions are generic; see the 'README' file for 18 | instructions specific to this package. Some packages provide this 19 | 'INSTALL' file but do not implement all of the features documented 20 | below. The lack of an optional feature in a given package is not 21 | necessarily a bug. More recommendations for GNU packages can be found 22 | in *note Makefile Conventions: (standards)Makefile Conventions. 23 | 24 | The 'configure' shell script attempts to guess correct values for 25 | various system-dependent variables used during compilation. It uses 26 | those values to create a 'Makefile' in each directory of the package. 27 | It may also create one or more '.h' files containing system-dependent 28 | definitions. Finally, it creates a shell script 'config.status' that 29 | you can run in the future to recreate the current configuration, and a 30 | file 'config.log' containing compiler output (useful mainly for 31 | debugging 'configure'). 32 | 33 | It can also use an optional file (typically called 'config.cache' and 34 | enabled with '--cache-file=config.cache' or simply '-C') that saves the 35 | results of its tests to speed up reconfiguring. Caching is disabled by 36 | default to prevent problems with accidental use of stale cache files. 37 | 38 | If you need to do unusual things to compile the package, please try 39 | to figure out how 'configure' could check whether to do them, and mail 40 | diffs or instructions to the address given in the 'README' so they can 41 | be considered for the next release. If you are using the cache, and at 42 | some point 'config.cache' contains results you don't want to keep, you 43 | may remove or edit it. 44 | 45 | The file 'configure.ac' (or 'configure.in') is used to create 46 | 'configure' by a program called 'autoconf'. You need 'configure.ac' if 47 | you want to change it or regenerate 'configure' using a newer version of 48 | 'autoconf'. 49 | 50 | The simplest way to compile this package is: 51 | 52 | 1. 'cd' to the directory containing the package's source code and type 53 | './configure' to configure the package for your system. 54 | 55 | Running 'configure' might take a while. While running, it prints 56 | some messages telling which features it is checking for. 57 | 58 | 2. Type 'make' to compile the package. 59 | 60 | 3. Optionally, type 'make check' to run any self-tests that come with 61 | the package, generally using the just-built uninstalled binaries. 62 | 63 | 4. Type 'make install' to install the programs and any data files and 64 | documentation. When installing into a prefix owned by root, it is 65 | recommended that the package be configured and built as a regular 66 | user, and only the 'make install' phase executed with root 67 | privileges. 68 | 69 | 5. Optionally, type 'make installcheck' to repeat any self-tests, but 70 | this time using the binaries in their final installed location. 71 | This target does not install anything. Running this target as a 72 | regular user, particularly if the prior 'make install' required 73 | root privileges, verifies that the installation completed 74 | correctly. 75 | 76 | 6. You can remove the program binaries and object files from the 77 | source code directory by typing 'make clean'. To also remove the 78 | files that 'configure' created (so you can compile the package for 79 | a different kind of computer), type 'make distclean'. There is 80 | also a 'make maintainer-clean' target, but that is intended mainly 81 | for the package's developers. If you use it, you may have to get 82 | all sorts of other programs in order to regenerate files that came 83 | with the distribution. 84 | 85 | 7. Often, you can also type 'make uninstall' to remove the installed 86 | files again. In practice, not all packages have tested that 87 | uninstallation works correctly, even though it is required by the 88 | GNU Coding Standards. 89 | 90 | 8. Some packages, particularly those that use Automake, provide 'make 91 | distcheck', which can by used by developers to test that all other 92 | targets like 'make install' and 'make uninstall' work correctly. 93 | This target is generally not run by end users. 94 | 95 | Compilers and Options 96 | ===================== 97 | 98 | Some systems require unusual options for compilation or linking that 99 | the 'configure' script does not know about. Run './configure --help' 100 | for details on some of the pertinent environment variables. 101 | 102 | You can give 'configure' initial values for configuration parameters 103 | by setting variables in the command line or in the environment. Here is 104 | an example: 105 | 106 | ./configure CC=c99 CFLAGS=-g LIBS=-lposix 107 | 108 | *Note Defining Variables::, for more details. 109 | 110 | Compiling For Multiple Architectures 111 | ==================================== 112 | 113 | You can compile the package for more than one kind of computer at the 114 | same time, by placing the object files for each architecture in their 115 | own directory. To do this, you can use GNU 'make'. 'cd' to the 116 | directory where you want the object files and executables to go and run 117 | the 'configure' script. 'configure' automatically checks for the source 118 | code in the directory that 'configure' is in and in '..'. This is known 119 | as a "VPATH" build. 120 | 121 | With a non-GNU 'make', it is safer to compile the package for one 122 | architecture at a time in the source code directory. After you have 123 | installed the package for one architecture, use 'make distclean' before 124 | reconfiguring for another architecture. 125 | 126 | On MacOS X 10.5 and later systems, you can create libraries and 127 | executables that work on multiple system types--known as "fat" or 128 | "universal" binaries--by specifying multiple '-arch' options to the 129 | compiler but only a single '-arch' option to the preprocessor. Like 130 | this: 131 | 132 | ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ 133 | CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ 134 | CPP="gcc -E" CXXCPP="g++ -E" 135 | 136 | This is not guaranteed to produce working output in all cases, you 137 | may have to build one architecture at a time and combine the results 138 | using the 'lipo' tool if you have problems. 139 | 140 | Installation Names 141 | ================== 142 | 143 | By default, 'make install' installs the package's commands under 144 | '/usr/local/bin', include files under '/usr/local/include', etc. You 145 | can specify an installation prefix other than '/usr/local' by giving 146 | 'configure' the option '--prefix=PREFIX', where PREFIX must be an 147 | absolute file name. 148 | 149 | You can specify separate installation prefixes for 150 | architecture-specific files and architecture-independent files. If you 151 | pass the option '--exec-prefix=PREFIX' to 'configure', the package uses 152 | PREFIX as the prefix for installing programs and libraries. 153 | Documentation and other data files still use the regular prefix. 154 | 155 | In addition, if you use an unusual directory layout you can give 156 | options like '--bindir=DIR' to specify different values for particular 157 | kinds of files. Run 'configure --help' for a list of the directories 158 | you can set and what kinds of files go in them. In general, the default 159 | for these options is expressed in terms of '${prefix}', so that 160 | specifying just '--prefix' will affect all of the other directory 161 | specifications that were not explicitly provided. 162 | 163 | The most portable way to affect installation locations is to pass the 164 | correct locations to 'configure'; however, many packages provide one or 165 | both of the following shortcuts of passing variable assignments to the 166 | 'make install' command line to change installation locations without 167 | having to reconfigure or recompile. 168 | 169 | The first method involves providing an override variable for each 170 | affected directory. For example, 'make install 171 | prefix=/alternate/directory' will choose an alternate location for all 172 | directory configuration variables that were expressed in terms of 173 | '${prefix}'. Any directories that were specified during 'configure', 174 | but not in terms of '${prefix}', must each be overridden at install time 175 | for the entire installation to be relocated. The approach of makefile 176 | variable overrides for each directory variable is required by the GNU 177 | Coding Standards, and ideally causes no recompilation. However, some 178 | platforms have known limitations with the semantics of shared libraries 179 | that end up requiring recompilation when using this method, particularly 180 | noticeable in packages that use GNU Libtool. 181 | 182 | The second method involves providing the 'DESTDIR' variable. For 183 | example, 'make install DESTDIR=/alternate/directory' will prepend 184 | '/alternate/directory' before all installation names. The approach of 185 | 'DESTDIR' overrides is not required by the GNU Coding Standards, and 186 | does not work on platforms that have drive letters. On the other hand, 187 | it does better at avoiding recompilation issues, and works well even 188 | when some directory options were not specified in terms of '${prefix}' 189 | at 'configure' time. 190 | 191 | Optional Features 192 | ================= 193 | 194 | If the package supports it, you can cause programs to be installed 195 | with an extra prefix or suffix on their names by giving 'configure' the 196 | option '--program-prefix=PREFIX' or '--program-suffix=SUFFIX'. 197 | 198 | Some packages pay attention to '--enable-FEATURE' options to 199 | 'configure', where FEATURE indicates an optional part of the package. 200 | They may also pay attention to '--with-PACKAGE' options, where PACKAGE 201 | is something like 'gnu-as' or 'x' (for the X Window System). The 202 | 'README' should mention any '--enable-' and '--with-' options that the 203 | package recognizes. 204 | 205 | For packages that use the X Window System, 'configure' can usually 206 | find the X include and library files automatically, but if it doesn't, 207 | you can use the 'configure' options '--x-includes=DIR' and 208 | '--x-libraries=DIR' to specify their locations. 209 | 210 | Some packages offer the ability to configure how verbose the 211 | execution of 'make' will be. For these packages, running './configure 212 | --enable-silent-rules' sets the default to minimal output, which can be 213 | overridden with 'make V=1'; while running './configure 214 | --disable-silent-rules' sets the default to verbose, which can be 215 | overridden with 'make V=0'. 216 | 217 | Particular systems 218 | ================== 219 | 220 | On HP-UX, the default C compiler is not ANSI C compatible. If GNU CC 221 | is not installed, it is recommended to use the following options in 222 | order to use an ANSI C compiler: 223 | 224 | ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" 225 | 226 | and if that doesn't work, install pre-built binaries of GCC for HP-UX. 227 | 228 | HP-UX 'make' updates targets which have the same time stamps as their 229 | prerequisites, which makes it generally unusable when shipped generated 230 | files such as 'configure' are involved. Use GNU 'make' instead. 231 | 232 | On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot 233 | parse its '' header file. The option '-nodtk' can be used as a 234 | workaround. If GNU CC is not installed, it is therefore recommended to 235 | try 236 | 237 | ./configure CC="cc" 238 | 239 | and if that doesn't work, try 240 | 241 | ./configure CC="cc -nodtk" 242 | 243 | On Solaris, don't put '/usr/ucb' early in your 'PATH'. This 244 | directory contains several dysfunctional programs; working variants of 245 | these programs are available in '/usr/bin'. So, if you need '/usr/ucb' 246 | in your 'PATH', put it _after_ '/usr/bin'. 247 | 248 | On Haiku, software installed for all users goes in '/boot/common', 249 | not '/usr/local'. It is recommended to use the following options: 250 | 251 | ./configure --prefix=/boot/common 252 | 253 | Specifying the System Type 254 | ========================== 255 | 256 | There may be some features 'configure' cannot figure out 257 | automatically, but needs to determine by the type of machine the package 258 | will run on. Usually, assuming the package is built to be run on the 259 | _same_ architectures, 'configure' can figure that out, but if it prints 260 | a message saying it cannot guess the machine type, give it the 261 | '--build=TYPE' option. TYPE can either be a short name for the system 262 | type, such as 'sun4', or a canonical name which has the form: 263 | 264 | CPU-COMPANY-SYSTEM 265 | 266 | where SYSTEM can have one of these forms: 267 | 268 | OS 269 | KERNEL-OS 270 | 271 | See the file 'config.sub' for the possible values of each field. If 272 | 'config.sub' isn't included in this package, then this package doesn't 273 | need to know the machine type. 274 | 275 | If you are _building_ compiler tools for cross-compiling, you should 276 | use the option '--target=TYPE' to select the type of system they will 277 | produce code for. 278 | 279 | If you want to _use_ a cross compiler, that generates code for a 280 | platform different from the build platform, you should specify the 281 | "host" platform (i.e., that on which the generated programs will 282 | eventually be run) with '--host=TYPE'. 283 | 284 | Sharing Defaults 285 | ================ 286 | 287 | If you want to set default values for 'configure' scripts to share, 288 | you can create a site shell script called 'config.site' that gives 289 | default values for variables like 'CC', 'cache_file', and 'prefix'. 290 | 'configure' looks for 'PREFIX/share/config.site' if it exists, then 291 | 'PREFIX/etc/config.site' if it exists. Or, you can set the 292 | 'CONFIG_SITE' environment variable to the location of the site script. 293 | A warning: not all 'configure' scripts look for a site script. 294 | 295 | Defining Variables 296 | ================== 297 | 298 | Variables not defined in a site shell script can be set in the 299 | environment passed to 'configure'. However, some packages may run 300 | configure again during the build, and the customized values of these 301 | variables may be lost. In order to avoid this problem, you should set 302 | them in the 'configure' command line, using 'VAR=value'. For example: 303 | 304 | ./configure CC=/usr/local2/bin/gcc 305 | 306 | causes the specified 'gcc' to be used as the C compiler (unless it is 307 | overridden in the site shell script). 308 | 309 | Unfortunately, this technique does not work for 'CONFIG_SHELL' due to an 310 | Autoconf limitation. Until the limitation is lifted, you can use this 311 | workaround: 312 | 313 | CONFIG_SHELL=/bin/bash ./configure CONFIG_SHELL=/bin/bash 314 | 315 | 'configure' Invocation 316 | ====================== 317 | 318 | 'configure' recognizes the following options to control how it 319 | operates. 320 | 321 | '--help' 322 | '-h' 323 | Print a summary of all of the options to 'configure', and exit. 324 | 325 | '--help=short' 326 | '--help=recursive' 327 | Print a summary of the options unique to this package's 328 | 'configure', and exit. The 'short' variant lists options used only 329 | in the top level, while the 'recursive' variant lists options also 330 | present in any nested packages. 331 | 332 | '--version' 333 | '-V' 334 | Print the version of Autoconf used to generate the 'configure' 335 | script, and exit. 336 | 337 | '--cache-file=FILE' 338 | Enable the cache: use and save the results of the tests in FILE, 339 | traditionally 'config.cache'. FILE defaults to '/dev/null' to 340 | disable caching. 341 | 342 | '--config-cache' 343 | '-C' 344 | Alias for '--cache-file=config.cache'. 345 | 346 | '--quiet' 347 | '--silent' 348 | '-q' 349 | Do not print messages saying which checks are being made. To 350 | suppress all normal output, redirect it to '/dev/null' (any error 351 | messages will still be shown). 352 | 353 | '--srcdir=DIR' 354 | Look for the package's source code in directory DIR. Usually 355 | 'configure' can determine that directory automatically. 356 | 357 | '--prefix=DIR' 358 | Use DIR as the installation prefix. *note Installation Names:: for 359 | more details, including other options available for fine-tuning the 360 | installation locations. 361 | 362 | '--no-create' 363 | '-n' 364 | Run the configure checks, but stop before creating any output 365 | files. 366 | 367 | 'configure' also accepts some other, not widely useful, options. Run 368 | 'configure --help' for more details. 369 | -------------------------------------------------------------------------------- /mod_authnz_jwt.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 2016 Anthony Deroche 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #include 19 | #include 20 | 21 | // RFC 7519 compliant library 22 | #include "jwt.h" 23 | #include 24 | //JSON library 25 | #include "jansson.h" 26 | 27 | #include "apr_strings.h" 28 | #include "apr_lib.h" 29 | #include "apr_base64.h" 30 | 31 | #include "ap_config.h" 32 | #include "httpd.h" 33 | #include "http_config.h" 34 | #include "http_core.h" 35 | #include "http_log.h" 36 | #include "http_protocol.h" 37 | #include "http_request.h" 38 | #include "ap_provider.h" 39 | #include "util_cookies.h" 40 | 41 | #include "mod_auth.h" 42 | 43 | #define JWT_LOGIN_HANDLER "jwt-login-handler" 44 | #define JWT_LOGOUT_HANDLER "jwt-login-handler" 45 | #define USER_INDEX 0 46 | #define PASSWORD_INDEX 1 47 | #define FORM_SIZE 512 48 | #define MAX_KEY_LEN 16384 49 | 50 | #define DEFAULT_EXP_DELAY 1800 51 | #define DEFAULT_NBF_DELAY 0 52 | #define DEFAULT_LEEWAY 0 53 | 54 | #define DEFAULT_FORM_USERNAME "user" 55 | #define DEFAULT_FORM_PASSWORD "password" 56 | #define DEFAULT_ATTRIBUTE_USERNAME "user" 57 | #define DEFAULT_SIGNATURE_ALGORITHM "HS256" 58 | #define DEFAULT_TOKEN_NAME "token" 59 | #define DEFAULT_COOKIE_NAME "AuthToken" 60 | #define DEFAULT_COOKIE_ATTR "Secure;HttpOnly;SameSite" 61 | #define DEFAULT_COOKIE_REMOVE 1 62 | 63 | #define JSON_DELIVERY "Json" 64 | #define COOKIE_DELIVERY "Cookie" 65 | #define DEFAULT_DELIVERY_TYPE JSON_DELIVERY 66 | 67 | 68 | 69 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CONFIGURATION STRUCTURE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 70 | 71 | typedef struct { 72 | authn_provider_list *providers; 73 | 74 | const char* signature_algorithm; 75 | int signature_algorithm_set; 76 | 77 | const char* signature_shared_secret; 78 | int signature_shared_secret_set; 79 | 80 | const char* signature_public_key_file; 81 | int signature_public_key_file_set; 82 | 83 | const char* signature_private_key_file; 84 | int signature_private_key_file_set; 85 | 86 | int exp_delay; 87 | int exp_delay_set; 88 | 89 | int nbf_delay; 90 | int nbf_delay_set; 91 | 92 | int leeway; 93 | int leeway_set; 94 | 95 | const char* iss; 96 | int iss_set; 97 | 98 | const char* aud; 99 | int aud_set; 100 | 101 | const char* form_username; 102 | int form_username_set; 103 | 104 | const char* form_password; 105 | int form_password_set; 106 | 107 | const char* attribute_username; 108 | int attribute_username_set; 109 | 110 | const char* delivery_type; 111 | int delivery_type_set; 112 | 113 | const char* token_name; 114 | int token_name_set; 115 | 116 | const char* cookie_name; 117 | int cookie_name_set; 118 | 119 | const char* cookie_attr; 120 | int cookie_attr_set; 121 | 122 | int cookie_remove; 123 | int cookie_remove_set; 124 | 125 | char *dir; 126 | 127 | } auth_jwt_config_rec; 128 | 129 | typedef enum { 130 | dir_signature_algorithm, 131 | dir_signature_shared_secret, 132 | dir_signature_public_key_file, 133 | dir_signature_private_key_file, 134 | dir_exp_delay, 135 | dir_nbf_delay, 136 | dir_iss, 137 | dir_aud, 138 | dir_leeway, 139 | dir_form_username, 140 | dir_form_password, 141 | dir_attribute_username, 142 | dir_delivery_type, 143 | dir_token_name, 144 | dir_cookie_name, 145 | dir_cookie_attr, 146 | dir_cookie_remove, 147 | } jwt_directive; 148 | 149 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS HEADERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 150 | 151 | 152 | static void *create_auth_jwt_dir_config(apr_pool_t *p, char *d); 153 | static void *create_auth_jwt_config(apr_pool_t * p, server_rec *s); 154 | static void *merge_auth_jwt_dir_config(apr_pool_t *p, void* basev, void* addv); 155 | static void *merge_auth_jwt_config(apr_pool_t *p, void* basev, void* addv); 156 | static void register_hooks(apr_pool_t * p); 157 | 158 | static const char *add_authn_provider(cmd_parms * cmd, void *config, const char *arg); 159 | static const char *set_jwt_param(cmd_parms * cmd, void* config, const char* value); 160 | static const char *set_jwt_int_param(cmd_parms * cmd, void* config, const char* value); 161 | static const char* get_config_value(request_rec *r, jwt_directive directive); 162 | static const int get_config_int_value(request_rec *r, jwt_directive directive); 163 | 164 | static const char *jwt_parse_config(cmd_parms *cmd, const char *require_line, const void **parsed_require_line); 165 | static authz_status jwtclaim_check_authorization(request_rec *r, const char* require_args, const void *parsed_require_args); 166 | static authz_status jwtclaimarray_check_authorization(request_rec *r, const char* require_args, const void *parsed_require_args); 167 | static const authz_provider authz_jwtclaim_provider = { 168 | &jwtclaim_check_authorization, 169 | &jwt_parse_config 170 | }; 171 | static const authz_provider authz_jwtclaimarray_provider = { 172 | &jwtclaimarray_check_authorization, 173 | &jwt_parse_config 174 | }; 175 | 176 | static int auth_jwt_login_handler(request_rec *r); 177 | static int check_authn(request_rec *r, const char *username, const char *password); 178 | static int create_token(request_rec *r, char** token_str, const char* username); 179 | 180 | static int auth_jwt_authn_with_token(request_rec *r); 181 | 182 | static void get_encode_key(request_rec* r, const char* algorithm, unsigned char* key, unsigned int* keylen); 183 | static void get_decode_key(request_rec* r, unsigned char* key, unsigned int* keylen); 184 | static int token_check(request_rec *r, jwt_t **jwt, const char *token, const unsigned char *key, unsigned int keylen); 185 | static int token_decode(jwt_t **jwt, const char* token, const unsigned char *key, unsigned int keylen); 186 | static int token_new(jwt_t **jwt); 187 | static const char* token_get_claim(jwt_t *token, const char* claim); 188 | static long token_get_claim_int(jwt_t *token, const char* claim); 189 | static int token_add_claim(jwt_t *jwt, const char *claim, const char *val); 190 | static int token_add_claim_int(jwt_t *jwt, const char *claim, long val); 191 | static void token_free(jwt_t *token); 192 | static int token_set_alg(request_rec *r, jwt_t *jwt, const char* alg, const unsigned char *key, unsigned int keylen); 193 | static char *token_encode_str(jwt_t *jwt); 194 | static char** token_get_claim_array_of_string(request_rec* r, jwt_t *token, const char* claim, int* len); 195 | static json_t* token_get_claim_array(request_rec* r, jwt_t *token, const char* claim); 196 | static json_t* token_get_claim_json(request_rec* r, jwt_t *token, const char* claim); 197 | static const char* token_get_alg(jwt_t *jwt); 198 | static jwt_alg_t parse_alg(const char* signature_algorithm); 199 | 200 | 201 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECLARE DIRECTIVES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 202 | 203 | static const command_rec auth_jwt_cmds[] = 204 | { 205 | 206 | AP_INIT_TAKE1("AuthJWTSignatureAlgorithm", set_jwt_param, (void *)dir_signature_algorithm, RSRC_CONF|OR_AUTHCFG, 207 | "The algorithm to use to sign tokens"), 208 | AP_INIT_TAKE1("AuthJWTSignatureSharedSecret", set_jwt_param, (void *)dir_signature_shared_secret, RSRC_CONF|OR_AUTHCFG, 209 | "The shared secret to use to sign tokens with HMACs"), 210 | AP_INIT_TAKE1("AuthJWTSignaturePublicKeyFile", set_jwt_param, (void *)dir_signature_public_key_file, RSRC_CONF|OR_AUTHCFG, 211 | "The file containing public key used to check signatures"), 212 | AP_INIT_TAKE1("AuthJWTSignaturePrivateKeyFile", set_jwt_param, (void *)dir_signature_private_key_file, RSRC_CONF|OR_AUTHCFG, 213 | "The file containing private key used to sign tokens"), 214 | AP_INIT_TAKE1("AuthJWTIss", set_jwt_param, (void *)dir_iss, RSRC_CONF|OR_AUTHCFG, 215 | "The issuer of delievered tokens"), 216 | AP_INIT_TAKE1("AuthJWTAud", set_jwt_param, (void *)dir_aud, RSRC_CONF|OR_AUTHCFG, 217 | "The audience of delivered tokens"), 218 | AP_INIT_TAKE1("AuthJWTExpDelay", set_jwt_int_param, (void *)dir_exp_delay, RSRC_CONF|OR_AUTHCFG, 219 | "The time delay in seconds after which delivered tokens are considered invalid"), 220 | AP_INIT_TAKE1("AuthJWTNbfDelay", set_jwt_int_param, (void *)dir_nbf_delay, RSRC_CONF|OR_AUTHCFG, 221 | "The time delay in seconds before which delivered tokens must not be processed"), 222 | AP_INIT_TAKE1("AuthJWTLeeway", set_jwt_int_param, (void *)dir_leeway, RSRC_CONF|OR_AUTHCFG, 223 | "The leeway to account for clock skew in token validation process"), 224 | AP_INIT_ITERATE("AuthJWTProvider", add_authn_provider, NULL, OR_AUTHCFG, 225 | "Specify the auth providers for a directory or location"), 226 | AP_INIT_TAKE1("AuthJWTFormUsername", set_jwt_param, (void *)dir_form_username, RSRC_CONF|OR_AUTHCFG, 227 | "The name of the field containing the username in authentication process"), 228 | AP_INIT_TAKE1("AuthJWTFormPassword", set_jwt_param, (void *)dir_form_password, RSRC_CONF|OR_AUTHCFG, 229 | "The name of the field containing the password in authentication process"), 230 | AP_INIT_TAKE1("AuthJWTAttributeUsername", set_jwt_param, (void *)dir_attribute_username, RSRC_CONF|OR_AUTHCFG, 231 | "The name of the attribute containing the username in the token"), 232 | AP_INIT_TAKE1("AuthJWTDeliveryType", set_jwt_param, (void *)dir_delivery_type, RSRC_CONF|OR_AUTHCFG, 233 | "Type of token delivery Json (default) or Cookie"), 234 | AP_INIT_TAKE1("AuthJWTTokenName", set_jwt_param, (void *)dir_token_name, RSRC_CONF|OR_AUTHCFG, 235 | "Token name to use when using JSON delivery"), 236 | AP_INIT_TAKE1("AuthJWTCookieName", set_jwt_param, (void *)dir_cookie_name, RSRC_CONF|OR_AUTHCFG, 237 | "Cookie name to use when using cookie delivery"), 238 | AP_INIT_TAKE1("AuthJWTCookieAttr", set_jwt_param, (void *)dir_cookie_attr, RSRC_CONF|OR_AUTHCFG, 239 | "semi-colon separated attributes for cookie when using cookie delivery. default: "DEFAULT_COOKIE_ATTR), 240 | AP_INIT_TAKE1("AuthJWTRemoveCookie", set_jwt_int_param, (void *)dir_cookie_remove, RSRC_CONF|OR_AUTHCFG, 241 | "Remove cookie from the headers, and thus keep it private from the backend. default: 1"), 242 | {NULL} 243 | }; 244 | 245 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DEFAULT CONFIGURATION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 246 | 247 | /* PER DIR CONFIGURATION */ 248 | static void *create_auth_jwt_dir_config(apr_pool_t *p, char *d){ 249 | auth_jwt_config_rec *conf = (auth_jwt_config_rec*) apr_pcalloc(p, sizeof(*conf)); 250 | conf->dir = d; 251 | 252 | conf->signature_algorithm_set = 0; 253 | conf->signature_shared_secret_set = 0; 254 | conf->signature_public_key_file_set = 0; 255 | conf->signature_private_key_file_set = 0; 256 | conf->exp_delay_set = 0; 257 | conf->nbf_delay_set = 0; 258 | conf->leeway_set = 0; 259 | conf->iss_set = 0; 260 | conf->aud_set = 0; 261 | conf->form_username_set=0; 262 | conf->form_password_set=0; 263 | conf->attribute_username_set=0; 264 | conf->delivery_type_set=0; 265 | conf->token_name_set=0; 266 | conf->cookie_name_set=0; 267 | conf->cookie_attr_set=0; 268 | conf->cookie_remove_set=0; 269 | 270 | return (void *)conf; 271 | } 272 | 273 | /* GLOBAL CONFIGURATION */ 274 | static void *create_auth_jwt_config(apr_pool_t * p, server_rec *s){ 275 | 276 | auth_jwt_config_rec *conf = (auth_jwt_config_rec*) apr_pcalloc(p, sizeof(*conf)); 277 | 278 | conf->signature_algorithm_set = 0; 279 | conf->signature_shared_secret_set = 0; 280 | conf->signature_public_key_file_set = 0; 281 | conf->signature_private_key_file_set = 0; 282 | conf->exp_delay_set = 0; 283 | conf->nbf_delay_set = 0; 284 | conf->leeway_set = 0; 285 | conf->iss_set = 0; 286 | conf->aud_set = 0; 287 | conf->form_username_set=0; 288 | conf->form_password_set=0; 289 | conf->attribute_username_set=0; 290 | conf->delivery_type_set=0; 291 | conf->token_name_set=0; 292 | conf->cookie_name_set=0; 293 | conf->cookie_attr_set=0; 294 | conf->cookie_remove_set=0; 295 | 296 | return (void *)conf; 297 | } 298 | 299 | static void* merge_auth_jwt_dir_config(apr_pool_t *p, void* basev, void* addv){ 300 | auth_jwt_config_rec *base = (auth_jwt_config_rec *)basev; 301 | auth_jwt_config_rec *add = (auth_jwt_config_rec *)addv; 302 | auth_jwt_config_rec *new = (auth_jwt_config_rec *) apr_pcalloc(p, sizeof(auth_jwt_config_rec)); 303 | 304 | new->providers = !add->providers ? base->providers : add->providers; 305 | new->signature_algorithm = (add->signature_algorithm_set == 0) ? base->signature_algorithm : add->signature_algorithm; 306 | new->signature_algorithm_set = base->signature_algorithm_set || add->signature_algorithm_set; 307 | 308 | new->signature_shared_secret = (add->signature_shared_secret_set == 0) ? base->signature_shared_secret : add->signature_shared_secret; 309 | new->signature_shared_secret_set = base->signature_shared_secret_set || add->signature_shared_secret_set; 310 | new->signature_public_key_file = (add->signature_public_key_file_set == 0) ? base->signature_public_key_file : add->signature_public_key_file; 311 | new->signature_public_key_file_set = base->signature_public_key_file_set || add->signature_public_key_file_set; 312 | new->signature_private_key_file = (add->signature_private_key_file_set == 0) ? base->signature_private_key_file : add->signature_private_key_file; 313 | new->signature_private_key_file_set = base->signature_private_key_file_set || add->signature_private_key_file_set; 314 | 315 | new->exp_delay = (add->exp_delay_set == 0) ? base->exp_delay : add->exp_delay; 316 | new->exp_delay_set = base->exp_delay_set || add->exp_delay_set; 317 | new->nbf_delay = (add->nbf_delay_set == 0) ? base->nbf_delay : add->nbf_delay; 318 | new->nbf_delay_set = base->nbf_delay_set || add->nbf_delay_set; 319 | new->leeway = (add->leeway_set == 0) ? base->leeway : add->leeway; 320 | new->leeway_set = base->leeway_set || add->leeway_set; 321 | new->iss = (add->iss_set == 0) ? base->iss : add->iss; 322 | new->iss_set = base->iss_set || add->iss_set; 323 | new->aud = (add->aud_set == 0) ? base->aud : add->aud; 324 | new->aud_set = base->aud_set || add->aud_set; 325 | new->form_username = (add->form_username_set == 0) ? base->form_username : add->form_username; 326 | new->form_username_set = base->form_username_set || add->form_username_set; 327 | new->form_password = (add->form_password_set == 0) ? base->form_password : add->form_password; 328 | new->form_password_set = base->form_password_set || add->form_password_set; 329 | new->attribute_username = (add->attribute_username_set == 0) ? base->attribute_username : add->attribute_username; 330 | new->attribute_username_set = base->attribute_username_set || add->attribute_username_set; 331 | new->delivery_type = (add->delivery_type_set == 0) ? base->delivery_type : add->delivery_type; 332 | new->delivery_type_set = base->delivery_type_set || add->delivery_type_set; 333 | new->token_name = (add->token_name_set == 0) ? base->token_name : add->token_name; 334 | new->token_name_set= base->token_name_set || add->token_name_set; 335 | new->cookie_name = (add->cookie_name_set == 0) ? base->cookie_name : add->cookie_name; 336 | new->cookie_name_set= base->cookie_name_set || add->cookie_name_set; 337 | new->cookie_attr = (add->cookie_attr_set == 0) ? base->cookie_attr : add->cookie_attr; 338 | new->cookie_attr_set= base->cookie_attr_set || add->cookie_attr_set; 339 | new->cookie_remove = (add->cookie_remove_set == 0) ? base->cookie_remove : add->cookie_remove; 340 | new->cookie_remove_set= base->cookie_remove_set || add->cookie_remove_set; 341 | return (void*)new; 342 | } 343 | 344 | static void* merge_auth_jwt_config(apr_pool_t *p, void* basev, void* addv){ 345 | return merge_auth_jwt_dir_config(p, basev, addv); 346 | } 347 | 348 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECLARE MODULE IN HTTPD CORE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 349 | 350 | AP_DECLARE_MODULE(auth_jwt) = { 351 | STANDARD20_MODULE_STUFF, 352 | create_auth_jwt_dir_config, 353 | merge_auth_jwt_dir_config, 354 | create_auth_jwt_config, 355 | merge_auth_jwt_config, 356 | auth_jwt_cmds, 357 | register_hooks 358 | }; 359 | 360 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FILL OUT CONF STRUCTURES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 361 | 362 | static const char* get_config_value(request_rec *r, jwt_directive directive){ 363 | 364 | auth_jwt_config_rec *dconf = (auth_jwt_config_rec *) ap_get_module_config(r->per_dir_config, &auth_jwt_module); 365 | 366 | auth_jwt_config_rec *sconf = (auth_jwt_config_rec *) ap_get_module_config(r->server->module_config, &auth_jwt_module); 367 | const char* value; 368 | 369 | switch ((jwt_directive) directive) { 370 | case dir_signature_algorithm: 371 | if(dconf->signature_algorithm_set && dconf->signature_algorithm){ 372 | value = dconf->signature_algorithm; 373 | }else if(sconf->signature_algorithm){ 374 | value = sconf->signature_algorithm; 375 | }else{ 376 | return DEFAULT_SIGNATURE_ALGORITHM; 377 | } 378 | break; 379 | case dir_signature_shared_secret: 380 | if(dconf->signature_shared_secret_set && dconf->signature_shared_secret){ 381 | value = dconf->signature_shared_secret; 382 | }else if(sconf->signature_shared_secret_set && sconf->signature_shared_secret){ 383 | value = sconf->signature_shared_secret; 384 | }else{ 385 | return NULL; 386 | } 387 | break; 388 | case dir_signature_public_key_file: 389 | if(dconf->signature_public_key_file_set && dconf->signature_public_key_file){ 390 | value = dconf->signature_public_key_file; 391 | }else if(sconf->signature_public_key_file_set && sconf->signature_public_key_file){ 392 | value = sconf->signature_public_key_file; 393 | }else{ 394 | return NULL; 395 | } 396 | break; 397 | case dir_signature_private_key_file: 398 | if(dconf->signature_private_key_file_set && dconf->signature_private_key_file){ 399 | value = dconf->signature_private_key_file; 400 | }else if(sconf->signature_private_key_file_set && sconf->signature_private_key_file){ 401 | value = sconf->signature_private_key_file; 402 | }else{ 403 | return NULL; 404 | } 405 | break; 406 | case dir_iss: 407 | if(dconf->iss_set && dconf->iss){ 408 | value = (void*)dconf->iss; 409 | }else if(sconf->iss_set && sconf->iss){ 410 | value = (void*)sconf->iss; 411 | }else{ 412 | return NULL; 413 | } 414 | break; 415 | case dir_aud: 416 | if(dconf->aud_set && dconf->aud){ 417 | value = dconf->aud; 418 | }else if(sconf->iss_set && sconf->aud){ 419 | value = sconf->aud; 420 | }else{ 421 | return NULL; 422 | } 423 | break; 424 | case dir_form_username: 425 | if(dconf->form_username_set && dconf->form_username){ 426 | value = dconf->form_username; 427 | }else if(sconf->form_username_set && sconf->form_username){ 428 | value = sconf->form_username; 429 | }else{ 430 | return DEFAULT_FORM_USERNAME; 431 | } 432 | break; 433 | case dir_form_password: 434 | if(dconf->form_password_set && dconf->form_password){ 435 | value = dconf->form_password; 436 | }else if(sconf->form_password_set && sconf->form_password){ 437 | value = sconf->form_password; 438 | }else{ 439 | return DEFAULT_FORM_PASSWORD; 440 | } 441 | break; 442 | case dir_attribute_username: 443 | if(dconf->attribute_username_set && dconf->attribute_username){ 444 | value = dconf->attribute_username; 445 | }else if(sconf->attribute_username_set && sconf->attribute_username){ 446 | value = sconf->attribute_username; 447 | }else{ 448 | return DEFAULT_ATTRIBUTE_USERNAME; 449 | } 450 | break; 451 | case dir_delivery_type: 452 | if(dconf->delivery_type_set && dconf->delivery_type){ 453 | value = dconf->delivery_type; 454 | }else if(sconf->delivery_type_set && sconf->delivery_type){ 455 | value = sconf->delivery_type; 456 | }else{ 457 | return DEFAULT_DELIVERY_TYPE; 458 | } 459 | break; 460 | case dir_token_name: 461 | if(dconf->token_name_set && dconf->token_name){ 462 | value = dconf->token_name; 463 | }else if(sconf->token_name_set && sconf->token_name){ 464 | value = sconf->token_name; 465 | }else{ 466 | return DEFAULT_TOKEN_NAME; 467 | } 468 | break; 469 | case dir_cookie_name: 470 | if(dconf->cookie_name_set && dconf->cookie_name){ 471 | value = dconf->cookie_name; 472 | }else if(sconf->cookie_name_set && sconf->cookie_name){ 473 | value = sconf->cookie_name; 474 | }else{ 475 | return DEFAULT_COOKIE_NAME; 476 | } 477 | break; 478 | case dir_cookie_attr: 479 | if(dconf->cookie_attr_set && dconf->cookie_attr){ 480 | value = dconf->cookie_attr; 481 | }else if(sconf->cookie_attr_set && sconf->cookie_attr){ 482 | value = sconf->cookie_attr; 483 | }else{ 484 | return DEFAULT_COOKIE_ATTR; 485 | } 486 | break; 487 | default: 488 | return NULL; 489 | } 490 | return value; 491 | } 492 | 493 | static const int get_config_int_value(request_rec *r, jwt_directive directive){ 494 | auth_jwt_config_rec *dconf = (auth_jwt_config_rec *) ap_get_module_config(r->per_dir_config, &auth_jwt_module); 495 | 496 | auth_jwt_config_rec *sconf = (auth_jwt_config_rec *) ap_get_module_config(r->server->module_config, &auth_jwt_module); 497 | 498 | int value; 499 | switch ((jwt_directive) directive) { 500 | case dir_exp_delay: 501 | if(dconf->exp_delay_set){ 502 | value = dconf->exp_delay; 503 | }else if(sconf->exp_delay_set){ 504 | value = sconf->exp_delay; 505 | }else{ 506 | return DEFAULT_EXP_DELAY; 507 | } 508 | break; 509 | case dir_nbf_delay: 510 | if(dconf->nbf_delay_set){ 511 | value = dconf->nbf_delay; 512 | }else if(sconf->nbf_delay_set){ 513 | value = sconf->nbf_delay; 514 | }else{ 515 | return DEFAULT_NBF_DELAY; 516 | } 517 | break; 518 | case dir_leeway: 519 | if(dconf->leeway){ 520 | value = dconf->leeway; 521 | }else if(sconf->leeway_set){ 522 | value = sconf->leeway; 523 | }else{ 524 | return DEFAULT_LEEWAY; 525 | } 526 | break; 527 | case dir_cookie_remove: 528 | if(dconf->cookie_remove_set){ 529 | value = dconf->cookie_remove; 530 | }else if(sconf->cookie_remove_set){ 531 | value = sconf->cookie_remove; 532 | }else{ 533 | return DEFAULT_COOKIE_REMOVE; 534 | } 535 | break; 536 | } 537 | return (const int)value; 538 | } 539 | 540 | 541 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ REGISTER HOOKS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 542 | 543 | static void register_hooks(apr_pool_t * p){ 544 | ap_hook_handler(auth_jwt_login_handler, NULL, NULL, APR_HOOK_MIDDLE); 545 | ap_hook_check_authn(auth_jwt_authn_with_token, NULL, NULL, APR_HOOK_MIDDLE, AP_AUTH_INTERNAL_PER_CONF); 546 | ap_register_auth_provider(p, AUTHZ_PROVIDER_GROUP, "jwt-claim", AUTHZ_PROVIDER_VERSION, &authz_jwtclaim_provider, AP_AUTH_INTERNAL_PER_CONF); 547 | ap_register_auth_provider(p, AUTHZ_PROVIDER_GROUP, "jwt-claim-array", AUTHZ_PROVIDER_VERSION, &authz_jwtclaimarray_provider, AP_AUTH_INTERNAL_PER_CONF); 548 | } 549 | 550 | 551 | 552 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DIRECTIVE HANDLERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 553 | 554 | static const char *add_authn_provider(cmd_parms * cmd, void *config, const char *arg) 555 | { 556 | auth_jwt_config_rec *conf = (auth_jwt_config_rec *) config; 557 | authn_provider_list *newp; 558 | 559 | newp = apr_pcalloc(cmd->pool, sizeof(authn_provider_list)); 560 | newp->provider_name = arg; 561 | 562 | newp->provider = ap_lookup_provider(AUTHN_PROVIDER_GROUP, newp->provider_name, AUTHN_PROVIDER_VERSION); 563 | 564 | if (newp->provider == NULL) { 565 | return apr_psprintf(cmd->pool,"Unknown Authn provider: %s", newp->provider_name); 566 | } 567 | 568 | if (!newp->provider->check_password) { 569 | return apr_psprintf(cmd->pool, "The '%s' Authn provider doesn't support JWT authentication", newp->provider_name); 570 | } 571 | 572 | if (!conf->providers) { 573 | conf->providers = newp; 574 | } 575 | else { 576 | authn_provider_list *last = conf->providers; 577 | 578 | while (last->next) { 579 | last = last->next; 580 | } 581 | last->next = newp; 582 | } 583 | 584 | return NULL; 585 | } 586 | 587 | static const char *set_jwt_param(cmd_parms * cmd, void* config, const char* value){ 588 | 589 | auth_jwt_config_rec *conf; 590 | if(!cmd->path){ 591 | conf = (auth_jwt_config_rec *) ap_get_module_config(cmd->server->module_config, &auth_jwt_module); 592 | }else{ 593 | conf = (auth_jwt_config_rec *) config; 594 | } 595 | 596 | switch ((jwt_directive) cmd->info) { 597 | case dir_signature_algorithm: 598 | conf->signature_algorithm = value; 599 | conf->signature_algorithm_set = 1; 600 | break; 601 | case dir_signature_shared_secret: 602 | conf->signature_shared_secret = value; 603 | conf->signature_shared_secret_set = 1; 604 | break; 605 | case dir_signature_public_key_file: 606 | conf->signature_public_key_file = value; 607 | conf->signature_public_key_file_set = 1; 608 | break; 609 | case dir_signature_private_key_file: 610 | conf->signature_private_key_file = value; 611 | conf->signature_private_key_file_set = 1; 612 | break; 613 | case dir_iss: 614 | conf->iss = value; 615 | conf->iss_set = 1; 616 | break; 617 | case dir_aud: 618 | conf->aud = value; 619 | conf->aud_set = 1; 620 | break; 621 | case dir_form_username: 622 | conf->form_username = value; 623 | conf->form_username_set = 1; 624 | break; 625 | case dir_form_password: 626 | conf->form_password = value; 627 | conf->form_password_set = 1; 628 | break; 629 | case dir_attribute_username: 630 | conf->attribute_username = value; 631 | conf->attribute_username_set = 1; 632 | break; 633 | case dir_delivery_type: 634 | if(strcmp(value, JSON_DELIVERY) || strcmp(value, COOKIE_DELIVERY)) { 635 | conf->delivery_type = value; 636 | conf->delivery_type_set = 1; 637 | } else { 638 | apr_psprintf(cmd->pool, "Invalid delivery type, must be %s or %s (case sensitive). Fallback to Json.", JSON_DELIVERY, COOKIE_DELIVERY); 639 | } 640 | break; 641 | case dir_token_name: 642 | conf->token_name = value; 643 | conf->token_name_set = 1; 644 | break; 645 | case dir_cookie_name: 646 | if(ap_cookie_check_string(value) == APR_SUCCESS) { 647 | conf->cookie_name = value; 648 | conf->cookie_name_set = 1; 649 | } else { 650 | apr_psprintf(cmd->pool, "Invalid cookie name: \"%s\". Fallback to default: \"%s\".", value, DEFAULT_COOKIE_NAME); 651 | } 652 | break; 653 | case dir_cookie_attr: 654 | conf->cookie_attr = value; 655 | conf->cookie_attr_set = 1; 656 | break; 657 | } 658 | 659 | return NULL; 660 | } 661 | 662 | static const char *set_jwt_int_param(cmd_parms * cmd, void* config, const char* value){ 663 | 664 | auth_jwt_config_rec *conf; 665 | if(!cmd->path){ 666 | conf = (auth_jwt_config_rec *) ap_get_module_config(cmd->server->module_config, &auth_jwt_module); 667 | }else{ 668 | conf = (auth_jwt_config_rec *) config; 669 | } 670 | 671 | const char *digit; 672 | for (digit = value; *digit; ++digit) { 673 | if (!apr_isdigit(*digit)) { 674 | return "Argument must be numeric!"; 675 | } 676 | } 677 | 678 | switch ((long) cmd->info) { 679 | case dir_exp_delay: 680 | conf->exp_delay = atoi(value); 681 | conf->exp_delay_set = 1; 682 | break; 683 | case dir_nbf_delay: 684 | conf->nbf_delay = atoi(value); 685 | conf->nbf_delay_set = 1; 686 | break; 687 | case dir_leeway: 688 | conf->leeway = atoi(value); 689 | conf->leeway_set = 1; 690 | break; 691 | case dir_cookie_remove: 692 | conf->cookie_remove = atoi(value); 693 | conf->cookie_remove_set = 1; 694 | break; 695 | } 696 | return NULL; 697 | } 698 | 699 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AUTHZ HANDLERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 700 | 701 | static const char *jwt_parse_config(cmd_parms *cmd, const char *require_line, const void **parsed_require_line){ 702 | const char *expr_err = NULL; 703 | ap_expr_info_t *expr; 704 | 705 | expr = ap_expr_parse_cmd(cmd, require_line, AP_EXPR_FLAG_STRING_RESULT, &expr_err, NULL); 706 | if(expr_err) 707 | return apr_pstrcat(cmd->temp_pool, "Cannot parse expression in require line: ", expr_err, NULL); 708 | 709 | *parsed_require_line = expr; 710 | return NULL; 711 | } 712 | 713 | static authz_status jwtclaim_check_authorization(request_rec *r, const char* require_args, const void *parsed_require_args){ 714 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55100) 715 | "auth_jwt require jwt-claim: checking authorization..."); 716 | if(!r->user){ 717 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55101) 718 | "auth_jwt authorize: no user found..."); 719 | return AUTHZ_DENIED_NO_USER; 720 | } 721 | const char* err = NULL; 722 | const ap_expr_info_t *expr = parsed_require_args; 723 | const char* require = ap_expr_str_exec(r, expr, &err); 724 | if(err){ 725 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55102) 726 | "auth_jwt authorize: require jwt-claim: Can't evaluate expression: %s",err); 727 | return AUTHZ_DENIED; 728 | } 729 | 730 | char *w, *value; 731 | 732 | while(require[0]){ 733 | w = ap_getword(r->pool, &require, '='); 734 | value = ap_getword_conf(r->pool, &require); 735 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55103) 736 | "auth_jwt authorize: checking claim %s has value %s", w, value); 737 | const char* real_value = token_get_claim((jwt_t*)apr_table_get(r->notes, "jwt"), w); 738 | if(real_value != NULL && apr_strnatcasecmp((const char*)real_value, (const char*)value) == 0){ 739 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55104) 740 | "auth_jwt authorize: require jwt-claim: authorization successful for claim %s=%s", w, value); 741 | return AUTHZ_GRANTED; 742 | }else{ 743 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55105) 744 | "auth_jwt authorize: require jwt-claim: authorization failed for claim %s=%s", w, value); 745 | } 746 | } 747 | 748 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55106) 749 | "auth_jwt authorize: require jwt-claim: authorization failed"); 750 | 751 | return AUTHZ_DENIED; 752 | } 753 | 754 | static authz_status jwtclaimarray_check_authorization(request_rec *r, const char* require_args, const void *parsed_require_args){ 755 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55107) 756 | "auth_jwt require jwt-claim-array: checking authorization..."); 757 | if(!r->user){ 758 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55108) 759 | "auth_jwt authorize: no user found..."); 760 | return AUTHZ_DENIED_NO_USER; 761 | } 762 | const char* err = NULL; 763 | const ap_expr_info_t *expr = parsed_require_args; 764 | const char* require = ap_expr_str_exec(r, expr, &err); 765 | if(err){ 766 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55109) 767 | "auth_jwt authorize: require jwt-claim: Can't evaluate expression: %s",err); 768 | return AUTHZ_DENIED; 769 | } 770 | 771 | char *w, *value; 772 | 773 | jwt_t* jwt = (jwt_t*)apr_table_get(r->notes, "jwt"); 774 | if(jwt == NULL){ 775 | return HTTP_INTERNAL_SERVER_ERROR; 776 | } 777 | 778 | while(require[0]){ 779 | w = ap_getword(r->pool, &require, '='); 780 | value = ap_getword_conf(r->pool, &require); 781 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55110) 782 | "auth_jwt authorize: checking claim %s has value %s", w, value); 783 | int len; 784 | char** array_values = token_get_claim_array_of_string(r, jwt, w, &len); 785 | if(array_values != NULL){ 786 | int i; 787 | for(i=0;ihandler || strcmp(r->handler, JWT_LOGIN_HANDLER)){ 813 | return DECLINED; 814 | } 815 | 816 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55200) 817 | "auth_jwt authn: authentication handler is handling authentication"); 818 | 819 | int res; 820 | char* buffer; 821 | apr_off_t len; 822 | apr_size_t size; 823 | int rv; 824 | 825 | if(r->method_number != M_POST){ 826 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55201) 827 | "auth_jwt authn: the " JWT_LOGIN_HANDLER " only supports the POST method for %s", r->uri); 828 | return HTTP_METHOD_NOT_ALLOWED; 829 | } 830 | 831 | const char* content_type = apr_table_get(r->headers_in, "Content-Type"); 832 | if(!content_type || strcmp(content_type, "application/x-www-form-urlencoded")!=0){ 833 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55202) 834 | "auth_jwt authn: content type must be x-www-form-urlencoded"); 835 | return HTTP_UNSUPPORTED_MEDIA_TYPE; 836 | } 837 | 838 | apr_array_header_t *pairs = NULL; 839 | res = ap_parse_form_data(r, NULL, &pairs, -1, FORM_SIZE); 840 | if (res != OK) { 841 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55202) 842 | "auth_jwt authn: an error occured while parsing form data, aborting authentication"); 843 | return res; 844 | } 845 | 846 | char* fields[] = {(char *)get_config_value(r, dir_form_username), (char *)get_config_value(r, dir_form_password)}; 847 | 848 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55203) 849 | "auth_jwt authn: reading fields %s and %s", fields[0], fields[1]); 850 | 851 | char* sent_values[2]; 852 | 853 | int i; 854 | while (pairs && !apr_is_empty_array(pairs)) { 855 | ap_form_pair_t *pair = (ap_form_pair_t *) apr_array_pop(pairs); 856 | for(i=0;i<2;i++){ 857 | if (fields[i] && !strcmp(pair->name, fields[i]) && &sent_values[i]) { 858 | apr_brigade_length(pair->value, 1, &len); 859 | size = (apr_size_t) len; 860 | buffer = apr_palloc(r->pool, size + 1); 861 | apr_brigade_flatten(pair->value, buffer, &size); 862 | buffer[len] = 0; 863 | sent_values[i] = buffer; 864 | } 865 | } 866 | } 867 | 868 | for(i=0;i<2;i++){ 869 | if(!sent_values[i]){ 870 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55204) 871 | "auth_jwt authn: the expected parameter %s is missing, aborting authentication", fields[i]); 872 | return HTTP_UNAUTHORIZED; 873 | } 874 | } 875 | 876 | r->user = sent_values[USER_INDEX]; 877 | 878 | rv = check_authn(r, sent_values[USER_INDEX], sent_values[PASSWORD_INDEX]); 879 | 880 | if(rv == OK){ 881 | char* token; 882 | rv = create_token(r, &token, sent_values[USER_INDEX]); 883 | if(rv == OK){ 884 | char* delivery_type = (char *)get_config_value(r, dir_delivery_type); 885 | 886 | if (delivery_type && strcmp(delivery_type, COOKIE_DELIVERY) == 0) { 887 | char* cookie_name = (char *)get_config_value(r, dir_cookie_name); 888 | char* cookie_attr = (char *)get_config_value(r, dir_cookie_attr); 889 | 890 | ap_cookie_write(r, cookie_name, token, cookie_attr, 0, 891 | r->headers_out, NULL); 892 | } else { 893 | char* token_name = (char *)get_config_value(r, dir_token_name); 894 | apr_table_setn(r->err_headers_out, "Content-Type", "application/json"); 895 | ap_rprintf(r, "{\"%s\":\"%s\"}", token_name, token); 896 | } 897 | 898 | free(token); 899 | } 900 | } 901 | 902 | return rv; 903 | } 904 | 905 | 906 | static int create_token(request_rec *r, char** token_str, const char* username){ 907 | 908 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55300) 909 | "auth_jwt: creating token..."); 910 | 911 | jwt_t *token; 912 | int allocate = token_new(&token); 913 | if(allocate!=0){ 914 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55301) 915 | "auth_jwt create_token: error while creating token: %s", strerror(errno)); 916 | return HTTP_INTERNAL_SERVER_ERROR; 917 | } 918 | 919 | char* signature_algorithm = (char *)get_config_value(r, dir_signature_algorithm); 920 | unsigned char sign_key[MAX_KEY_LEN] = { 0 }; 921 | unsigned int keylen; 922 | get_encode_key(r, signature_algorithm, sign_key, &keylen); 923 | 924 | if(keylen == 0){ 925 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55302) 926 | "auth_jwt create_token: key used for signature is empty"); 927 | token_free(token); 928 | return HTTP_INTERNAL_SERVER_ERROR; 929 | } 930 | 931 | char* iss = (char *)get_config_value(r, dir_iss); 932 | char* aud = (char *)get_config_value(r, dir_aud); 933 | int exp_delay = get_config_int_value(r, dir_exp_delay); 934 | int nbf_delay = get_config_int_value(r, dir_nbf_delay); 935 | 936 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55305) 937 | "auth_jwt create_token: using algorithm %s (key length=%d)...", signature_algorithm, keylen); 938 | 939 | if(token_set_alg(r, token, signature_algorithm, sign_key, keylen)!=0){ 940 | token_free(token); 941 | return HTTP_INTERNAL_SERVER_ERROR; 942 | } 943 | 944 | time_t now = time(NULL); 945 | time_t iat = now; 946 | time_t exp = now; 947 | time_t nbf = now; 948 | 949 | 950 | if(exp_delay >= 0){ 951 | exp += exp_delay; 952 | token_add_claim_int(token, "exp", (long)exp); 953 | } 954 | 955 | if(nbf_delay >= 0){ 956 | nbf += nbf_delay; 957 | token_add_claim_int(token, "nbf", (long)nbf); 958 | } 959 | 960 | token_add_claim_int(token, "iat", (long)iat); 961 | 962 | if(iss){ 963 | token_add_claim(token, "iss", iss); 964 | } 965 | 966 | if(aud){ 967 | token_add_claim(token, "aud", aud); 968 | } 969 | 970 | const char* username_attribute = (const char *)get_config_value(r, dir_attribute_username); 971 | 972 | token_add_claim(token, username_attribute, username); 973 | 974 | *token_str = token_encode_str(token); 975 | token_free(token); 976 | return OK; 977 | } 978 | 979 | static int check_authn(request_rec *r, const char *username, const char *password){ 980 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55220) 981 | "auth_jwt: authenticating user"); 982 | authn_status authn_result; 983 | authn_provider_list *current_provider; 984 | auth_jwt_config_rec *conf = ap_get_module_config(r->per_dir_config, &auth_jwt_module); 985 | 986 | current_provider = conf->providers; 987 | do { 988 | const authn_provider *provider; 989 | 990 | if (!current_provider) { 991 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55221) 992 | "no authn provider configured"); 993 | authn_result = AUTH_GENERAL_ERROR; 994 | break; 995 | } 996 | else { 997 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55222) 998 | "auth_jwt authn: using provider %s", current_provider->provider_name); 999 | provider = current_provider->provider; 1000 | apr_table_setn(r->notes, AUTHN_PROVIDER_NAME_NOTE, current_provider->provider_name); 1001 | } 1002 | 1003 | if (!username || !password) { 1004 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55223) 1005 | "auth_jwt authn: username or password is missing, cannot pursue authentication"); 1006 | authn_result = AUTH_USER_NOT_FOUND; 1007 | break; 1008 | } 1009 | 1010 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55224) 1011 | "auth_jwt authn: checking credentials..."); 1012 | authn_result = provider->check_password(r, username, password); 1013 | 1014 | apr_table_unset(r->notes, AUTHN_PROVIDER_NAME_NOTE); 1015 | 1016 | if (authn_result != AUTH_USER_NOT_FOUND) { 1017 | break; 1018 | } 1019 | 1020 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55225) 1021 | "auth_jwt authn: no user has been found, trying the next provider..."); 1022 | 1023 | if (!conf->providers) { 1024 | break; 1025 | } 1026 | 1027 | current_provider = current_provider->next; 1028 | } while (current_provider); 1029 | 1030 | if (authn_result != AUTH_GRANTED) { 1031 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55226) 1032 | "auth_jwt authn: credentials are not correct"); 1033 | int return_code; 1034 | 1035 | /*if (authn_result != AUTH_DENIED) && !(conf->authoritative)) 1036 | return DECLINED; 1037 | }*/ 1038 | 1039 | switch (authn_result) { 1040 | case AUTH_DENIED: 1041 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55227) 1042 | "user '%s': authentication failure for \"%s\": " 1043 | "password Mismatch", 1044 | username, r->uri); 1045 | return_code = HTTP_UNAUTHORIZED; 1046 | break; 1047 | case AUTH_USER_NOT_FOUND: 1048 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55228) 1049 | "user '%s' not found: %s", username, r->uri); 1050 | return_code = HTTP_UNAUTHORIZED; 1051 | break; 1052 | case AUTH_GENERAL_ERROR: 1053 | default: 1054 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55229) 1055 | "auth_jwt authn: an error occured in the authentication provider, aborting authentication"); 1056 | return_code = HTTP_INTERNAL_SERVER_ERROR; 1057 | break; 1058 | } 1059 | 1060 | return return_code; 1061 | } 1062 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55230) 1063 | "auth_jwt authn: credentials are correct"); 1064 | return OK; 1065 | } 1066 | 1067 | 1068 | /* 1069 | If we are configured to handle authentication, let's look up headers to find 1070 | whether or not 'Authorization' is set. If so, exepected format is 1071 | Authorization: Bearer json_web_token. Then we check if the token is valid. 1072 | */ 1073 | static int auth_jwt_authn_with_token(request_rec *r){ 1074 | const char *current_auth = NULL; 1075 | current_auth = ap_auth_type(r); 1076 | int rv; 1077 | 1078 | if (!current_auth || strncmp(current_auth, "jwt", 3) != 0) { 1079 | return DECLINED; 1080 | } 1081 | 1082 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55400) 1083 | "auth_jwt: checking authentication with token..."); 1084 | 1085 | /* We need an authentication realm. */ 1086 | if (!ap_auth_name(r)) { 1087 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55401) 1088 | "need AuthName: %s", r->uri); 1089 | return HTTP_INTERNAL_SERVER_ERROR; 1090 | } 1091 | 1092 | r->ap_auth_type = (char *) current_auth; 1093 | 1094 | const char* token_str = 0; 1095 | 1096 | const char* authSubType = current_auth + 3; 1097 | 1098 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55400) 1099 | "auth_jwt: authSubType %s", authSubType); 1100 | 1101 | // 0 wrong value, 2 bearer, 4 cookie, 6 both 1102 | const int delivery_type = (strlen(authSubType) == 0 || strcmp(authSubType, "-bearer") == 0) ? 2 : 1103 | strcmp(authSubType, "-cookie") == 0 ? 4 : 1104 | strcmp(authSubType, "-both") == 0 ? 6 : 1105 | 0; 1106 | 1107 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55400) 1108 | "auth_jwt: delivery_type %i", delivery_type); 1109 | 1110 | // todo use struct with some predefined static values 1111 | char* logCode = APLOGNO(55401); 1112 | char* logStr = "auth_jwt authn: unexpected error"; 1113 | char* errorStr = NULL; 1114 | 1115 | if (delivery_type == 0) { 1116 | return DECLINED; 1117 | } 1118 | 1119 | if(delivery_type & 2) { 1120 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55402) 1121 | "auth_jwt authn: reading Authorization header..."); 1122 | char* authorization_header = (char*)apr_table_get( r->headers_in, "Authorization"); 1123 | 1124 | if(authorization_header) { 1125 | if(strlen(authorization_header) > 7 && !strncmp(authorization_header, "Bearer ", 7)){ 1126 | token_str = authorization_header+7; 1127 | } else { 1128 | logCode = APLOGNO(55408); 1129 | logStr = "auth_jwt authn: type of Authorization header is not Bearer"; 1130 | errorStr = "error=\"invalid_request\", error_description=\"Authentication type must be Bearer\""; 1131 | } 1132 | } else { 1133 | logCode = APLOGNO(55404); 1134 | logStr = "auth_jwt authn: missing Authorization header, responding with WWW-Authenticate header..."; 1135 | } 1136 | } 1137 | 1138 | if(delivery_type & 4 && !token_str){ 1139 | int cookie_remove = get_config_int_value(r, dir_cookie_remove); 1140 | const char* cookie_name = (char *)get_config_value(r, dir_cookie_name); 1141 | const char* cookieToken; 1142 | 1143 | ap_cookie_read(r, cookie_name, &token_str, cookie_remove); 1144 | 1145 | if(!token_str) { 1146 | logCode = APLOGNO(55409); 1147 | logStr = "auth_jwt authn: missing authorization cookie"; 1148 | } 1149 | } 1150 | 1151 | if(!token_str) { 1152 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s%s", logCode, logStr); 1153 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, "realm=\"", ap_auth_name(r),"\"", errorStr, NULL)); 1154 | return HTTP_UNAUTHORIZED; 1155 | } 1156 | 1157 | unsigned char key[MAX_KEY_LEN] = { 0 }; 1158 | unsigned int keylen; 1159 | 1160 | get_decode_key(r, key, &keylen); 1161 | 1162 | if(keylen == 0){ 1163 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55403) 1164 | "auth_jwt authn: key used to check signature is empty"); 1165 | return HTTP_INTERNAL_SERVER_ERROR; 1166 | } 1167 | 1168 | jwt_t* token; 1169 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55405) 1170 | "auth_jwt authn: checking signature and fields correctness..."); 1171 | rv = token_check(r, &token, token_str, key, keylen); 1172 | 1173 | if(OK == rv){ 1174 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55406) 1175 | "auth_jwt authn: signature is correct"); 1176 | const char* found_alg = token_get_alg(token); 1177 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(55405) 1178 | "auth_jwt authn: algorithm found is %s", found_alg); 1179 | const char* attribute_username = (const char*)get_config_value(r, dir_attribute_username); 1180 | char* maybe_user = (char *)token_get_claim(token, attribute_username); 1181 | /* 1182 | * User claim claim is optional 1183 | if(maybe_user == NULL){ 1184 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55407) 1185 | "Username was not in token ('%s' attribute is expected)", attribute_username); 1186 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1187 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Username was not in token\"", 1188 | NULL)); 1189 | return HTTP_UNAUTHORIZED; 1190 | } 1191 | */ 1192 | apr_table_setn(r->notes, "jwt", (const char*)token); 1193 | if(maybe_user != NULL){ 1194 | r->user = maybe_user; 1195 | }else{ 1196 | r->user = "anonymous"; 1197 | } 1198 | return OK; 1199 | } else { 1200 | if(token) 1201 | token_free(token); 1202 | 1203 | return rv; 1204 | } 1205 | } 1206 | 1207 | 1208 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TOKEN OPERATIONS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 1209 | 1210 | static void get_encode_key(request_rec *r, const char* signature_algorithm, unsigned char* key, unsigned int* keylen){ 1211 | 1212 | if(strcmp(signature_algorithm, "HS512")==0 || strcmp(signature_algorithm, "HS384")==0 || strcmp(signature_algorithm, "HS256")==0){ 1213 | char* signature_shared_secret = (char*)get_config_value(r, dir_signature_shared_secret); 1214 | if(!signature_shared_secret){ 1215 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55501) 1216 | "You must specify AuthJWTSignatureSharedSecret directive in configuration with algorithm %s", signature_algorithm); 1217 | return; 1218 | } 1219 | apr_pool_t *base64_decode_pool; 1220 | apr_pool_create(&base64_decode_pool, NULL); 1221 | size_t decoded_len, buf_len = apr_base64_decode_len(signature_shared_secret); 1222 | char *decode_buf = apr_pcalloc(base64_decode_pool, buf_len); 1223 | decoded_len = apr_base64_decode(decode_buf, signature_shared_secret); /* was bin */ 1224 | memcpy(key, decode_buf, buf_len); 1225 | *keylen = decoded_len; 1226 | } 1227 | else if(strcmp(signature_algorithm, "RS512")==0 || strcmp(signature_algorithm, "RS384")==0 || strcmp(signature_algorithm, "RS256")==0 || 1228 | strcmp(signature_algorithm, "ES512")==0 || strcmp(signature_algorithm, "ES384")==0 || strcmp(signature_algorithm, "ES256")==0){ 1229 | char* signature_private_key_file = (char*)get_config_value(r, dir_signature_private_key_file); 1230 | if(!signature_private_key_file){ 1231 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55502) 1232 | "You must specify AuthJWTSignaturePrivateKeyFile directive in configuration with algorithm %s", signature_algorithm); 1233 | return; 1234 | } 1235 | apr_status_t rv; 1236 | apr_file_t* key_fd = NULL; 1237 | rv = apr_file_open(&key_fd, signature_private_key_file, APR_READ, APR_OS_DEFAULT, r->pool); 1238 | if(rv!=APR_SUCCESS){ 1239 | char error_buf[50]; 1240 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55503) 1241 | "Unable to open the file %s: %s", signature_private_key_file, apr_strerror(rv, error_buf, 50)); 1242 | return; 1243 | } 1244 | apr_size_t key_len; 1245 | rv = apr_file_read_full(key_fd, key, MAX_KEY_LEN, &key_len); 1246 | if(rv!=APR_SUCCESS && rv!=APR_EOF){ 1247 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55504) 1248 | "Error while reading the file %s", signature_private_key_file); 1249 | return; 1250 | } 1251 | apr_file_close(key_fd); 1252 | *keylen = (unsigned int)key_len; 1253 | } else { 1254 | //unknown algorithm 1255 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55505) 1256 | "Unknown algorithm %s", signature_algorithm); 1257 | } 1258 | } 1259 | 1260 | static void get_decode_key(request_rec *r, unsigned char* key, unsigned int* keylen){ 1261 | char* signature_public_key_file = (char*)get_config_value(r, dir_signature_public_key_file); 1262 | char* signature_shared_secret = (char*)get_config_value(r, dir_signature_shared_secret); 1263 | 1264 | if(!signature_shared_secret && !signature_public_key_file){ 1265 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55507) 1266 | "You must specify either AuthJWTSignatureSharedSecret directive or AuthJWTSignaturePublicKeyFile directive in configuration for decoding process"); 1267 | return; 1268 | } 1269 | 1270 | if(signature_shared_secret && signature_public_key_file){ 1271 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55507) 1272 | "Conflict in configuration: you must specify either AuthJWTSignatureSharedSecret directive or AuthJWTSignaturePublicKeyFile directive but not both in the same block"); 1273 | return; 1274 | } 1275 | 1276 | if(signature_shared_secret){ 1277 | apr_pool_t *base64_decode_pool; 1278 | apr_pool_create(&base64_decode_pool, NULL); 1279 | size_t decoded_len, buf_len = apr_base64_decode_len((const char*)signature_shared_secret); 1280 | char *decode_buf = apr_pcalloc(base64_decode_pool, buf_len); 1281 | decoded_len = apr_base64_decode(decode_buf, signature_shared_secret); 1282 | memcpy((char*)key, (const char*)decode_buf, decoded_len); 1283 | *keylen = (unsigned int)decoded_len; 1284 | } 1285 | else if(signature_public_key_file){ 1286 | apr_status_t rv; 1287 | apr_file_t* key_fd = NULL; 1288 | rv = apr_file_open(&key_fd, signature_public_key_file, APR_FOPEN_READ, APR_FPROT_OS_DEFAULT, r->pool); 1289 | if(rv!=APR_SUCCESS){ 1290 | char error_buf[50]; 1291 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55503) 1292 | "Unable to open the file %s: %s", signature_public_key_file, apr_strerror(rv, error_buf, 50)); 1293 | return; 1294 | } 1295 | apr_size_t key_len; 1296 | rv = apr_file_read_full(key_fd, key, MAX_KEY_LEN, &key_len); 1297 | if(rv!=APR_SUCCESS && rv!=APR_EOF){ 1298 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55510) 1299 | "Error while reading the file %s", signature_public_key_file); 1300 | return; 1301 | } 1302 | *keylen = (unsigned int)key_len; 1303 | apr_file_close(key_fd); 1304 | } 1305 | } 1306 | 1307 | static int token_new(jwt_t **jwt){ 1308 | return jwt_new(jwt); 1309 | } 1310 | 1311 | 1312 | static int token_check(request_rec *r, jwt_t **jwt, const char *token, const unsigned char *key, unsigned int keylen){ 1313 | 1314 | int decode_res = token_decode(jwt, token, key, keylen); 1315 | 1316 | if(decode_res != 0){ 1317 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55512)"Decoding process has failed, token is either malformed or signature is invalid"); 1318 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1319 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Token is malformed or signature is invalid\"", 1320 | NULL)); 1321 | return HTTP_UNAUTHORIZED; 1322 | } 1323 | 1324 | /* 1325 | Trunk of libjwt does not need this check because the bug is fixed 1326 | We should not accept token with provided alg none 1327 | */ 1328 | if(*jwt && jwt_get_alg(*jwt) == JWT_ALG_NONE){ 1329 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1330 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Token is malformed\"", 1331 | NULL)); 1332 | return HTTP_UNAUTHORIZED; 1333 | } 1334 | 1335 | const char* iss_config = (char *)get_config_value(r, dir_iss); 1336 | const char* aud_config = (char *)get_config_value(r, dir_aud); 1337 | int leeway = get_config_int_value(r, dir_leeway); 1338 | 1339 | const char* iss_to_check = token_get_claim(*jwt, "iss"); 1340 | if(iss_config && iss_to_check && strcmp(iss_config, iss_to_check)!=0){ 1341 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55513)"Token issuer does not match with configured issuer"); 1342 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1343 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Issuer is not valid\"", 1344 | NULL)); 1345 | return HTTP_UNAUTHORIZED; 1346 | } 1347 | 1348 | const char* aud_to_check = token_get_claim(*jwt, "aud"); 1349 | if(aud_config && aud_to_check && strcmp(aud_config, aud_to_check)!=0){ 1350 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55514)"Token audience does not match with configured audience"); 1351 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1352 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Audience is not valid\"", 1353 | NULL)); 1354 | return HTTP_UNAUTHORIZED; 1355 | } 1356 | 1357 | /* check exp */ 1358 | long exp = token_get_claim_int(*jwt, "exp"); 1359 | if(exp>0){ 1360 | time_t now = time(NULL); 1361 | if (exp + leeway < now){ 1362 | /* token expired */ 1363 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55516)"Token expired"); 1364 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1365 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Token expired\"", 1366 | NULL)); 1367 | return HTTP_UNAUTHORIZED; 1368 | } 1369 | } 1370 | 1371 | /* check nbf */ 1372 | long nbf = token_get_claim_int(*jwt, "nbf"); 1373 | if(nbf>0){ 1374 | time_t now = time(NULL); 1375 | if (nbf - leeway > now){ 1376 | /* token is too recent to be processed */ 1377 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55518)"Nbf check failed. Token can't be processed now"); 1378 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1379 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Token can't be processed now due to nbf field\"", 1380 | NULL)); 1381 | return HTTP_UNAUTHORIZED; 1382 | } 1383 | } 1384 | 1385 | /* 1386 | Do not accept other signature algorithms than configured 1387 | */ 1388 | const char* sig_config = (char *)get_config_value(r, dir_signature_algorithm); 1389 | if(*jwt && parse_alg(sig_config) != jwt_get_alg(*jwt)){ 1390 | apr_table_setn(r->err_headers_out, "WWW-Authenticate", apr_pstrcat(r->pool, 1391 | "Bearer realm=\"", ap_auth_name(r),"\", error=\"invalid_token\", error_description=\"Unsupported Signature Algorithm\"", 1392 | NULL)); 1393 | return HTTP_UNAUTHORIZED; 1394 | } 1395 | return OK; 1396 | } 1397 | 1398 | static int token_decode(jwt_t **jwt, const char* token, const unsigned char *key, unsigned int keylen){ 1399 | return jwt_decode(jwt, token, key, keylen); 1400 | } 1401 | 1402 | static char *token_encode_str(jwt_t *jwt){ 1403 | return jwt_encode_str(jwt); 1404 | } 1405 | 1406 | static int token_add_claim(jwt_t *jwt, const char *claim, const char *val){ 1407 | return jwt_add_grant(jwt, claim, val); 1408 | } 1409 | 1410 | static int token_add_claim_int(jwt_t *jwt, const char *claim, long val){ 1411 | return jwt_add_grant_int(jwt, claim, val); 1412 | } 1413 | 1414 | static const char* token_get_claim(jwt_t *token, const char* claim){ 1415 | return jwt_get_grant(token, claim); 1416 | } 1417 | 1418 | static long token_get_claim_int(jwt_t *token, const char* claim){ 1419 | return jwt_get_grant_int(token, claim); 1420 | } 1421 | 1422 | 1423 | static char** token_get_claim_array_of_string(request_rec *r, jwt_t *token, const char* claim, int* len){ 1424 | json_t* array = token_get_claim_array(r, token, claim); 1425 | if(!array){ 1426 | return NULL; 1427 | } 1428 | 1429 | int array_len = json_array_size(array); 1430 | char** values = (char**)apr_pcalloc(r->pool, array_len*sizeof(char*)); 1431 | int i; 1432 | for(i=0; ipool, strlen(string_value)+1*sizeof(char)); 1442 | strcpy(values[i], string_value); 1443 | } 1444 | json_decref(array); 1445 | *len = array_len; 1446 | return values; 1447 | } 1448 | 1449 | static json_t* token_get_claim_array(request_rec *r, jwt_t *token, const char* claim){ 1450 | json_t* array = token_get_claim_json(r, token, claim); 1451 | 1452 | if(!array){ 1453 | return NULL; 1454 | } 1455 | 1456 | if(!json_is_array(array)){ 1457 | json_decref(array); 1458 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55520)"Claim '%s' is not a JSON array", claim); 1459 | return NULL; 1460 | } 1461 | return array; 1462 | } 1463 | 1464 | static json_t* token_get_claim_json(request_rec *r, jwt_t *token, const char* claim){ 1465 | char* json_str = jwt_get_grants_json(token, claim); 1466 | if(json_str == NULL){ 1467 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55521)"Missing claim '%s' in token", claim); 1468 | return NULL; 1469 | } 1470 | json_error_t error; 1471 | json_t* json = json_loads(json_str, 0, &error); 1472 | 1473 | if(!json){ 1474 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55522)"Claim '%s' is not a JSON valid string: %s", claim, error.text); 1475 | return NULL; 1476 | } 1477 | 1478 | return json; 1479 | } 1480 | 1481 | static int token_set_alg(request_rec *r, jwt_t *jwt, const char* signature_algorithm, const unsigned char *key, unsigned int keylen){ 1482 | jwt_alg_t algorithm = parse_alg(signature_algorithm); 1483 | if(algorithm == JWT_ALG_NONE) { 1484 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(55304) 1485 | "Unknown algorithm %s", signature_algorithm); 1486 | return 1; 1487 | } 1488 | return jwt_set_alg(jwt, algorithm, key, keylen); 1489 | } 1490 | 1491 | static const char* token_get_alg(jwt_t *jwt){ 1492 | jwt_alg_t algorithm = jwt_get_alg(jwt); 1493 | switch(algorithm){ 1494 | case JWT_ALG_HS256: 1495 | return "HS256"; 1496 | case JWT_ALG_HS384: 1497 | return "HS384"; 1498 | case JWT_ALG_HS512: 1499 | return "HS512"; 1500 | case JWT_ALG_RS256: 1501 | return "RS256"; 1502 | case JWT_ALG_RS384: 1503 | return "RS384"; 1504 | case JWT_ALG_RS512: 1505 | return "RS512"; 1506 | case JWT_ALG_ES256: 1507 | return "ES256"; 1508 | case JWT_ALG_ES384: 1509 | return "ES384"; 1510 | case JWT_ALG_ES512: 1511 | return "ES512"; 1512 | default: 1513 | return NULL; 1514 | } 1515 | } 1516 | 1517 | static jwt_alg_t parse_alg(const char* signature_algorithm) { 1518 | jwt_alg_t algorithm; 1519 | if(!strcmp(signature_algorithm, "HS512")){ 1520 | algorithm = JWT_ALG_HS512; 1521 | }else if(!strcmp(signature_algorithm, "HS384")){ 1522 | algorithm = JWT_ALG_HS384; 1523 | }else if(!strcmp(signature_algorithm, "HS256")){ 1524 | algorithm = JWT_ALG_HS256; 1525 | }else if(!strcmp(signature_algorithm, "RS512")){ 1526 | algorithm = JWT_ALG_RS512; 1527 | }else if(!strcmp(signature_algorithm, "RS384")){ 1528 | algorithm = JWT_ALG_RS384; 1529 | }else if(!strcmp(signature_algorithm, "RS256")){ 1530 | algorithm = JWT_ALG_RS256; 1531 | }else if(!strcmp(signature_algorithm, "ES512")){ 1532 | algorithm = JWT_ALG_ES512; 1533 | }else if(!strcmp(signature_algorithm, "ES384")){ 1534 | algorithm = JWT_ALG_ES384; 1535 | }else if(!strcmp(signature_algorithm, "ES256")){ 1536 | algorithm = JWT_ALG_ES256; 1537 | }else{ 1538 | algorithm = JWT_ALG_NONE; 1539 | } 1540 | return algorithm; 1541 | } 1542 | 1543 | 1544 | static void token_free(jwt_t *token){ 1545 | jwt_free(token); 1546 | } 1547 | 1548 | --------------------------------------------------------------------------------