MitreJWTParser.java

/*
 * Copyright (c) 2002-2021, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.oauth2.jwt;

import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.JWSAlgorithm;

import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

import fr.paris.lutece.plugins.oauth2.business.AuthClientConf;
import fr.paris.lutece.plugins.oauth2.business.AuthServerConf;
import fr.paris.lutece.plugins.oauth2.business.IDToken;
import fr.paris.lutece.plugins.oauth2.business.Token;
import fr.paris.lutece.plugins.oauth2.web.Constants;

import org.apache.log4j.Logger;

import java.text.ParseException;

import java.util.Date;

/**
 * MitreJWTParser
 *
 * Some parts of the code come from the MITRE project and especialy from the class : com.plec.artistes.security.CustomOIDCAuthenticationFilter
 */
public class MitreJWTParser implements JWTParser
{
    // Allow for time sync issues by having a window of X seconds.
    private int _nTimeSkewAllowance = 300;

    /**
     * {@inheritDoc }
     */
    @Override
    public void parseJWT( Token token, AuthClientConf clientConfig, AuthServerConf serverConfig, String strStoredNonce, Logger logger )
            throws TokenValidationException
    {
        JWT jwt;
        IDToken idToken = new IDToken( );

        try
        {
            jwt = com.nimbusds.jwt.JWTParser.parse( token.getIdTokenString( ) );
        }
        catch( ParseException ex )
        {
            throw new TokenValidationException( "Unable to parse JWT : " + ex.getMessage( ), ex );
        }

        // validate our ID Token over a number of tests
        ReadOnlyJWTClaimsSet idClaims;

        try
        {
            idClaims = jwt.getJWTClaimsSet( );
        }
        catch( ParseException ex )
        {
            throw new TokenValidationException( "Unable to get Claims set from JWT : " + ex.getMessage( ), ex );
        }

        Algorithm tokenAlg = jwt.getHeader( ).getAlgorithm( );

        Algorithm clientAlg = clientConfig.getIdTokenSignedResponseAlg( );

        if ( clientAlg != null )
        {
            if ( !clientAlg.equals( tokenAlg ) )
            {
                throw new TokenValidationException( "Token algorithm " + tokenAlg + " does not match expected algorithm " + clientAlg );
            }
        }

        if ( jwt instanceof PlainJWT )
        {
            logger.debug( "ID token is a Plain JWT" );

            if ( clientAlg == null )
            {
                throw new TokenValidationException( "Unsigned ID tokens can only be used if explicitly configured in client." );
            }

            if ( ( tokenAlg != null ) && !tokenAlg.equals( JWSAlgorithm.NONE ) )
            {
                throw new TokenValidationException( "Unsigned token received, expected signature with " + tokenAlg );
            }
        }
        else
            if ( jwt instanceof SignedJWT )
            {
                logger.debug( "ID token is a signed JWT" );

                /*
                 * // check the signature JwtSigningAndValidationService jwtValidator = null;
                 * 
                 * SignedJWT signedIdToken = (SignedJWT) idToken;
                 * 
                 * if (tokenAlg.equals(JWSAlgorithm.HS256) || tokenAlg.equals(JWSAlgorithm.HS384) || tokenAlg.equals(JWSAlgorithm.HS512)) {
                 * 
                 * // generate one based on client secret jwtValidator = symmetricCacheService.getSymmetricValidtor( clientConfig.getClient()); } else { //
                 * otherwise load from the server's public key jwtValidator = validationServices.getValidator( serverConfig.getJwksUri() ); }
                 * 
                 * if (jwtValidator != null) { if (!jwtValidator.validateSignature(signedIdToken)) { throw new
                 * TokenValidationException("Signature validation failed"); } } else {
                 * _logger.error("No validation service found. Skipping signature validation"); throw new
                 * TokenValidationException("Unable to find an appropriate signature validator for ID Token."); }
                 */
            } // TODO: encrypted id tokens

        // check the issuer
        if ( idClaims.getIssuer( ) == null )
        {
            throw new TokenValidationException( "Id Token Issuer is null" );
        }
        else
            if ( !idClaims.getIssuer( ).equals( serverConfig.getIssuer( ) ) )
            {
                throw new TokenValidationException( "Issuers do not match, expected " + serverConfig.getIssuer( ) + " got " + idClaims.getIssuer( ) );
            }

        // check expiration
        if ( idClaims.getExpirationTime( ) == null )
        {
            throw new TokenValidationException( "Id Token does not have required expiration claim" );
        }
        else
        {
            // it's not null, see if it's expired
            Date now = new Date( System.currentTimeMillis( ) - ( _nTimeSkewAllowance * 1000 ) );

            if ( now.after( idClaims.getExpirationTime( ) ) )
            {
                throw new TokenValidationException( "Id Token is expired: " + idClaims.getExpirationTime( ) );
            }
        }

        // check not before
        if ( idClaims.getNotBeforeTime( ) != null )
        {
            Date now = new Date( System.currentTimeMillis( ) + ( _nTimeSkewAllowance * 1000 ) );

            if ( now.before( idClaims.getNotBeforeTime( ) ) )
            {
                throw new TokenValidationException( "Id Token not valid untill: " + idClaims.getNotBeforeTime( ) );
            }
        }

        // check issued at
        if ( idClaims.getIssueTime( ) == null )
        {
            throw new TokenValidationException( "Id Token does not have required issued-at claim" );
        }
        else
        {
            // since it's not null, see if it was issued in the future
            Date now = new Date( System.currentTimeMillis( ) + ( _nTimeSkewAllowance * 1000 ) );

            if ( now.before( idClaims.getIssueTime( ) ) )
            {
                throw new TokenValidationException( "Id Token was issued in the future: " + idClaims.getIssueTime( ) );
            }
        }

        // check audience
        if ( idClaims.getAudience( ) == null )
        {
            throw new TokenValidationException( "Id token audience is null" );
        }
        else
            if ( !idClaims.getAudience( ).contains( clientConfig.getClientId( ) ) )
            {
                throw new TokenValidationException( "Audience does not match, expected " + clientConfig.getClientId( ) + " got " + idClaims.getAudience( ) );
            }

        // compare the nonce to our stored claim
        String strNonce = null;

        try
        {
            strNonce = idClaims.getStringClaim( "nonce" );
        }
        catch( ParseException ex )
        {
            throw new TokenValidationException( "ID token did not contain a nonce claim." );
        }

        if ( ( strNonce == null ) || strNonce.equals( "" ) )
        {
            logger.error( "ID token did not contain a nonce claim." );

            throw new TokenValidationException( "ID token did not contain a nonce claim." );
        }

        if ( !strNonce.equals( strStoredNonce ) )
        {
            logger.error( "Possible replay attack detected! The comparison of the nonce in the returned " + "ID Token to the session "
                    + Constants.NONCE_SESSION_VARIABLE + " failed. Expected " + strStoredNonce + " got " + strNonce + "." );

            throw new TokenValidationException( "Possible replay attack detected! The comparison of the nonce in the returned " + "ID Token to the session "
                    + Constants.NONCE_SESSION_VARIABLE + " failed. Expected " + strStoredNonce + " got " + strNonce + "." );
        }

        logger.debug( "Nonce has been validated" );

        // Get IDP
        String strIdp;

        try
        {
            strIdp = idClaims.getStringClaim( "idp" );
        }
        catch( ParseException ex )
        {
            throw new TokenValidationException( "ID token did not contain an idp claim.", ex );
        }

        // Get ACR
        String strAcr;

        try
        {
            strAcr = idClaims.getStringClaim( "acr" );
        }
        catch( ParseException ex )
        {
            throw new TokenValidationException( "ID token did not contain an acr claim.", ex );
        }

        idToken.setNonce( strNonce );
        idToken.setSubject( idClaims.getSubject( ) );
        idToken.setIdProvider( strIdp );
        idToken.setExpiration( String.valueOf( idClaims.getExpirationTime( ).getTime( ) / 1000L ) );
        idToken.setIssueAt( String.valueOf( idClaims.getIssueTime( ).getTime( ) / 1000L ) );
        idToken.setIssuer( idClaims.getIssuer( ) );
        idToken.setAudience( idClaims.getAudience( ).get( 0 ) );
        idToken.setAcr( strAcr );
        logger.debug( "ID Token retrieved : " + idToken );

        token.setIdToken( idToken );
    }
}