OpenIDConfiguration.java

/*
 * Copyright (c) 2002-2025, 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.business;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonProperty;

import fr.paris.lutece.portal.service.util.AppException;

/**
 * OpenID Configuration
 * 
 * @see https://openid.net/specs/openid-connect-discovery-1_0.html
 * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
 * @since 2.0.0
 */
public class OpenIDConfiguration
{
    private String _strIssuer;
    private String _strAuthorizationEndpoint;
    private String _strTokenEndpoint;
    private String _strUserinfoEndpoint;
    private String _strJwksURI;
    private String _strRegistrationEndpoint;
    private String [ ] _scopesSupported;
    private String [ ] _responseTypesSupported;
    private String [ ] _responseModesSupported;
    private String [ ] _grantTypesSupported;
    private String [ ] _acrValuesSupported;
    private String [ ] _subjectTypesSupported;
    private String [ ] _idTokenSigningAlgValuesSupported;
    private String [ ] _idTokenEncryptionAlgValuesSupported;
    private String [ ] _idTokenEncryptionEncValuesSupported;
    private String [ ] _userinfoSigningAlgValuesSupported;
    private String [ ] _userinfoEncryptionAlgValuesSupported;
    private String [ ] _userinfoEncryptionEncValuesSupported;
    private String [ ] _requestObjectSigningAlgValuesSupported;
    private String [ ] _requestObjectEncryptionAlgValuesSupported;
    private String [ ] _requestObjectEncryptionEncValuesSupported;
    private String [ ] _tokenEndpointAuthMethodsSupported;
    private String [ ] _tokenEndpointAuthSigningAlgValuesSupported;
    private String [ ] _displayValuesSupported;
    private String [ ] _claimTypesSupported;
    private String [ ] _claimsSupported;
    private String _serviceDocumentation;
    private String [ ] _claimsLocalesSupported;
    private String [ ] _uiLocalesSupported;
    private boolean _claimsParameterSupported;
    private boolean _requestParameterSupported;
    private boolean _requestUIRParameterSupported = true;
    private boolean _requireRequestUIRRegistration;
    private String _opPolicyURI;
    private String _opTOSURI;
    private String _strEndSessionEndpoint;

    public String getIssuer( )
    {
        return _strIssuer;
    }

    public void setIssuer( String strIssuer )
    {
        _strIssuer = strIssuer;
    }

    @JsonProperty( "authorization_endpoint" )
    public String getAuthorizationEndpoint( )
    {
        return _strAuthorizationEndpoint;
    }

    @JsonProperty( "authorization_endpoint" )
    public void getAuthorizationEndpoint( String strAuthorizationEndpoint )
    {
        _strAuthorizationEndpoint = strAuthorizationEndpoint;
    }

    public String getTokenEndpoint( )
    {
        return _strTokenEndpoint;
    }

    @JsonProperty( "token_endpoint" )
    public void setTokenEndpoint( String strTokenEndpoint )
    {
        _strTokenEndpoint = strTokenEndpoint;
    }

    public String getUserinfoEndpoint( )
    {
        return _strUserinfoEndpoint;
    }

    @JsonProperty( "userinfo_endpoint" )
    public void setUserinfoEndpoint( String strUserinfoEndpoint )
    {
        _strUserinfoEndpoint = strUserinfoEndpoint;
    }

    public String getJwksURI( )
    {
        return _strJwksURI;
    }

    @JsonProperty( "jwks_uri" )
    public void setJwksUri( String strJwksURI )
    {
        _strJwksURI = strJwksURI;
    }

    public String getRegistrationEndpoint( )
    {
        return _strRegistrationEndpoint;
    }

    @JsonProperty( "registration_endpoint" )
    public void setRegistrationEndpoint( String strRegistrationEndpoint )
    {
        _strRegistrationEndpoint = strRegistrationEndpoint;
    }

    public String [ ] getScopesSupported( )
    {
        return _scopesSupported;
    }

    @JsonProperty( "scopes_supported" )
    public void setScopesSupported( String [ ] scopesSupported )
    {
        this._scopesSupported = scopesSupported;
    }

    public String [ ] getResponseTypesSupported( )
    {
        return _responseTypesSupported;
    }

    @JsonProperty( "response_types_supported" )
    public void setResponseTypesSupported( String [ ] responseTypesSupported )
    {
        this._responseTypesSupported = responseTypesSupported;
    }

    public String [ ] getResponseModesSupported( )
    {
        return _responseModesSupported;
    }

    @JsonProperty( "response_modes_supported" )
    public void setResponseModesSupported( String [ ] responseModesSupported )
    {
        this._responseModesSupported = responseModesSupported;
    }

    public String [ ] getGrantTypesSupported( )
    {
        return _grantTypesSupported;
    }

    @JsonProperty( "grant_types_supported" )
    public void setGrantTypesSupported( String [ ] grantTypesSupported )
    {
        this._grantTypesSupported = grantTypesSupported;
    }

    public String [ ] getAcrValuesSupported( )
    {
        return _acrValuesSupported;
    }

    @JsonProperty( "acr_values_supported" )
    public void setAcrValuesSupported( String [ ] acrValuesSupported )
    {
        this._acrValuesSupported = acrValuesSupported;
    }

    public String [ ] getSubjectTypesSupported( )
    {
        return _subjectTypesSupported;
    }

    @JsonProperty( "subject_types_supported" )
    public void setSubjectTypesSupported( String [ ] subjectTypesSupported )
    {
        this._subjectTypesSupported = subjectTypesSupported;
    }

    public String [ ] getIDTokenSigningAlgValuesSupported( )
    {
        return _idTokenSigningAlgValuesSupported;
    }

    @JsonProperty( "id_token_signing_alg_values_supported" )
    public void setIDTokenSigningAlgValuesSupported( String [ ] idTokenSigningAlgValuesSupported )
    {
        this._idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported;
    }

    public String [ ] getIDTokenEncryptionAlgValuesSupported( )
    {
        return _idTokenEncryptionAlgValuesSupported;
    }

    @JsonProperty( "id_token_encryption_alg_values_supported" )
    public void setIDTokenEncryptionAlgValuesSupported( String [ ] idTokenEncryptionAlgValuesSupported )
    {
        this._idTokenEncryptionAlgValuesSupported = idTokenEncryptionAlgValuesSupported;
    }

    public String [ ] getIDTokenEncryptionEncValuesSupported( )
    {
        return _idTokenEncryptionEncValuesSupported;
    }

    @JsonProperty( "id_token_encryption_enc_values_supported" )
    public void setIDTokenEncryptionEncValuesSupported( String [ ] _idTokenEncryptionEncValuesSupported )
    {
        this._idTokenEncryptionEncValuesSupported = _idTokenEncryptionEncValuesSupported;
    }

    public String [ ] getUserinfoSigningAlgValuesSupported( )
    {
        return _userinfoSigningAlgValuesSupported;
    }

    @JsonProperty( "userinfo_signing_alg_values_supported" )
    public void setUserinfoSigningAlgValuesSupported( String [ ] userinfoSigningAlgValuesSupported )
    {
        this._userinfoSigningAlgValuesSupported = userinfoSigningAlgValuesSupported;
    }

    public String [ ] getUserinfoEncryptionAlgValuesSupported( )
    {
        return _userinfoEncryptionAlgValuesSupported;
    }

    @JsonProperty( "userinfo_encryption_alg_values_supported" )
    public void setUserinfoEncryptionAlgValuesSupported( String [ ] userinfoEncryptionAlgValuesSupported )
    {
        this._userinfoEncryptionAlgValuesSupported = userinfoEncryptionAlgValuesSupported;
    }

    public String [ ] getUserinfoEncryptionEncValuesSupported( )
    {
        return _userinfoEncryptionEncValuesSupported;
    }

    @JsonProperty( "userinfo_encryption_enc_values_supported" )
    public void setUserinfoEncryptionEncValuesSupported( String [ ] userinfoEncryptionEncValuesSupported )
    {
        this._userinfoEncryptionEncValuesSupported = userinfoEncryptionEncValuesSupported;
    }

    public String [ ] getRequestObjectSigningAlgValuesSupported( )
    {
        return _requestObjectSigningAlgValuesSupported;
    }

    @JsonProperty( "request_object_signing_alg_values_supported" )
    public void setRequestObjectSigningAlgValuesSupported( String [ ] requestObjectSigningAlgValuesSupported )
    {
        this._requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported;
    }

    public String [ ] getRequestObjectEncryptionAlgValuesSupported( )
    {
        return _requestObjectEncryptionAlgValuesSupported;
    }

    @JsonProperty( "request_object_encryption_alg_values_supported" )
    public void setRequestObjectEncryptionAlgValuesSupported( String [ ] requestObjectEncryptionAlgValuesSupported )
    {
        this._requestObjectEncryptionAlgValuesSupported = requestObjectEncryptionAlgValuesSupported;
    }

    public String [ ] getRequestObjectEncryptionEncValuesSupported( )
    {
        return _requestObjectEncryptionEncValuesSupported;
    }

    @JsonProperty( "request_object_encryption_enc_values_supported" )
    public void setRequestObjectEncryptionEncValuesSupported( String [ ] requestObjectEncryptionEncValuesSupported )
    {
        this._requestObjectEncryptionEncValuesSupported = requestObjectEncryptionEncValuesSupported;
    }

    public String [ ] getTokenEndpointAuthMethodsSupported( )
    {
        return _tokenEndpointAuthMethodsSupported;
    }

    @JsonProperty( "token_endpoint_auth_methods_supported" )
    public void setTokenEndpointAuthMethodsSupported( String [ ] tokenEndpointAuthMethodsSupported )
    {
        this._tokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported;
    }

    public String [ ] getTokenEndpointAuthSigningAlgValuesSupported( )
    {
        return _tokenEndpointAuthSigningAlgValuesSupported;
    }

    @JsonProperty( "token_endpoint_auth_signing_alg_values_supported" )
    public void setTokenEndpointAuthSigningAlgValuesSupported( String [ ] tokenEndpointAuthSigningAlgValuesSupported )
    {
        this._tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported;
    }

    public String [ ] getDisplayValuesSupported( )
    {
        return _displayValuesSupported;
    }

    @JsonProperty( "display_values_supported" )
    public void setDisplayValuesSupported( String [ ] displayValuesSupported )
    {
        this._displayValuesSupported = displayValuesSupported;
    }

    public String [ ] getClaimTypesSupported( )
    {
        return _claimTypesSupported;
    }

    @JsonProperty( "claim_types_supported" )
    public void setClaimTypesSupported( String [ ] claimTypesSupported )
    {
        this._claimTypesSupported = claimTypesSupported;
    }

    public String [ ] getClaimsSupported( )
    {
        return _claimsSupported;
    }

    @JsonProperty( "claims_supported" )
    public void setClaimsSupported( String [ ] claimsSupported )
    {
        this._claimsSupported = claimsSupported;
    }

    public String getServiceDocumentation( )
    {
        return _serviceDocumentation;
    }

    @JsonProperty( "service_documentation" )
    public void setServiceDocumentation( String serviceDocumentation )
    {
        this._serviceDocumentation = serviceDocumentation;
    }

    public String [ ] getClaimsLocalesSupported( )
    {
        return _claimsLocalesSupported;
    }

    @JsonProperty( "claims_locales_supported" )
    public void setClaimsLocalesSupported( String [ ] claimsLocalesSupported )
    {
        this._claimsLocalesSupported = claimsLocalesSupported;
    }

    public String [ ] getUILocalesSupported( )
    {
        return _uiLocalesSupported;
    }

    @JsonProperty( "ui_locales_supported" )
    public void setUILocalesSupported( String [ ] uiLocalesSupported )
    {
        this._uiLocalesSupported = uiLocalesSupported;
    }

    public boolean isClaimsParameterSupported( )
    {
        return _claimsParameterSupported;
    }

    @JsonProperty( "claims_parameter_supported" )
    public void setClaimsParameterSupported( boolean claimsParameterSupported )
    {
        this._claimsParameterSupported = claimsParameterSupported;
    }

    public boolean isRequestParameterSupported( )
    {
        return _requestParameterSupported;
    }

    @JsonProperty( "request_parameter_supported" )
    public void setRequestParameterSupported( boolean requestParameterSupported )
    {
        this._requestParameterSupported = requestParameterSupported;
    }

    public boolean isRequestUIRParameterSupported( )
    {
        return _requestUIRParameterSupported;
    }

    @JsonProperty( "request_uri_parameter_supported" )
    public void setRequestUIRParameterSupported( boolean requestUIRParameterSupported )
    {
        this._requestUIRParameterSupported = requestUIRParameterSupported;
    }

    public boolean isRequireRequestUIRRegistration( )
    {
        return _requireRequestUIRRegistration;
    }

    @JsonProperty( "require_request_uri_registration" )
    public void setRequireRequestUIRRegistration( boolean requireRequestUIRRegistration )
    {
        this._requireRequestUIRRegistration = requireRequestUIRRegistration;
    }

    public String getOpPolicyURI( )
    {
        return _opPolicyURI;
    }

    @JsonProperty( "op_policy_uri" )
    public void setOpPolicyURI( String opPolicyURI )
    {
        this._opPolicyURI = opPolicyURI;
    }

    public String getOpTOSURI( )
    {
        return _opTOSURI;
    }

    @JsonProperty( "op_tos_uri" )
    public void setOpTOSURI( String opTOSURI )
    {
        this._opTOSURI = opTOSURI;
    }

    public String getEndSessionEndpoint( )
    {
        return _strEndSessionEndpoint;
    }

    @JsonProperty( "end_session_endpoint" )
    public void setEndSessionEndpoint( String strEndSessionEndpoint )
    {
        this._strEndSessionEndpoint = strEndSessionEndpoint;
    }

    /**
     * Validate the configuration.
     * 
     * @param strExpectedIssuer
     *            the expected issuer
     * @throws NullPointerException
     *             if a required parameter is absent
     * @throws AppException
     *             if another error is present
     */
    public void validate( String strExpectedIssuer )
    {
        validateIssuer( strExpectedIssuer );
        validateAuthorizationEndpoint( );
        validateTokenEndpoint( );
        validateUserinfoEndpoint( );
        validateJwksURI( );
        validateRegistrationEndpoint( );
        validateScopesSupported( );
        validateResponseTypeSupported( );
        validateSubjectTypesSupported( );
        validateIDTokenSigningAlgValuesSupported( );
        validateTokenEndpointAuthSigningAlgValuesSupported( );
        validateEndSessionEndpoint( );
    }

    private void validateIssuer( String strExpectedIssuer )
    {
        Objects.requireNonNull( _strIssuer, "issuer is required" );
        validateURI( _strIssuer, "issuer", false, false );
        if ( !_strIssuer.equals( strExpectedIssuer ) )
        {
            throw new AppException( "Expected issuer " + strExpectedIssuer + ", but got " + _strIssuer );
        }
    }

    private void validateAuthorizationEndpoint( )
    {
        Objects.requireNonNull( _strAuthorizationEndpoint, "Authorization endpoint is required" );
        validateURI( _strAuthorizationEndpoint, "Authorization endpoint" );
    }

    private void validateTokenEndpoint( )
    {
        if ( _strTokenEndpoint == null )
        {
            // FIXME This is REQUIRED unless only the Implicit Flow is used
            return;
        }
        validateURI( _strTokenEndpoint, "Token endpoint" );
    }

    private void validateUserinfoEndpoint( )
    {
        if ( _strUserinfoEndpoint == null )
        {
            return;
        }
        validateURI( _strUserinfoEndpoint, "Userinfo endpoint" );
    }

    private void validateJwksURI( )
    {
        Objects.requireNonNull( _strJwksURI, "JWKS URI is required" );
        validateURI( _strJwksURI, "JWKS URI" );
    }

    private void validateRegistrationEndpoint( )
    {
        if ( _strRegistrationEndpoint == null )
        {
            return;
        }
        validateURI( _strRegistrationEndpoint, "Registration endpoint" );
    }

    private void validateScopesSupported( )
    {
        // The server MUST support the openid scope value. Servers MAY choose not to advertise some supported scope values even when this parameter is used,
        // although those defined in [OpenID.Core] SHOULD be listed, if supported.
    }

    private void validateResponseTypeSupported( )
    {
        Objects.requireNonNull( _responseTypesSupported, "Response types supported is required" );
        for ( String type : _responseTypesSupported )
        {
            Objects.requireNonNull( type, "response type must not be null" );
        }
    }

    private void validateSubjectTypesSupported( )
    {
        Objects.requireNonNull( _subjectTypesSupported, "Subject types supported is required" );
        for ( String type : _subjectTypesSupported )
        {
            Objects.requireNonNull( type, "subject type must not be null" );
        }
    }

    private void validateTokenEndpointAuthSigningAlgValuesSupported( )
    {
        if ( _tokenEndpointAuthSigningAlgValuesSupported == null )
        {
            return;
        }
        if ( Arrays.stream( _tokenEndpointAuthSigningAlgValuesSupported ).anyMatch( alg -> "none".equals( alg ) ) )
        {
            throw new AppException( "The algorithm none MUST NOT be used for the token endpoint auth signing alg values supported" );
        }
    }

    private void validateEndSessionEndpoint( )
    {
        if ( _strEndSessionEndpoint == null )
        {
            return;
        }
        validateURI( _strEndSessionEndpoint, "end session endpoint" );
    }

    private void validateIDTokenSigningAlgValuesSupported( )
    {
        Objects.requireNonNull( _idTokenSigningAlgValuesSupported, "ID Token signing alg values supported is required" );
        if ( Arrays.stream( _idTokenSigningAlgValuesSupported ).noneMatch( alg -> "RS256".equals( alg ) ) )
        {
            throw new AppException( "The algorithm RS256 MUST be included in ID Token signing alg values supported" );
        }
    }

    private void validateURI( String strURI, String strFieldName )
    {
        validateURI( strURI, strFieldName, true, true );
    }

    private void validateURI( String strURI, String strFieldName, boolean queryAllowed, boolean fragmentAllowed )
    {
        try
        {
            URI theURI = new URI( strURI );
            if ( !theURI.getScheme( ).equals( "https" ) )
            {
                throw new AppException( strFieldName + " must be a URL using the https scheme" );
            }
            if ( !queryAllowed && theURI.getQuery( ) != null )
            {
                throw new AppException( strFieldName + " must be a URL with no query component" );
            }
            if ( !fragmentAllowed && theURI.getFragment( ) != null )
            {
                throw new AppException( strFieldName + " must be a URL with no fragment  component" );
            }
        }
        catch( URISyntaxException e )
        {
            throw new AppException( "Unable to validate URI <" + strURI + "> for field " + strFieldName + ": " + e.getMessage( ), e );
        }
    }
}