From 687f0ed0946aa70c3df9855f2c71b020986cf2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Oudot?= Date: Thu, 20 Nov 2014 14:03:32 +0000 Subject: [PATCH] Change configuration format to allow to define several OP (#183) --- .../Lemonldap/NG/Portal/AuthOpenIDConnect.pm | 109 +++++++++------ .../lib/Lemonldap/NG/Portal/_OpenIDConnect.pm | 125 +++++++++++------- ...70-Lemonldap-NG-Portal-AuthOpenIDConnect.t | 17 ++- 3 files changed, 152 insertions(+), 99 deletions(-) diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/AuthOpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/AuthOpenIDConnect.pm index 830589594..67b835099 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/AuthOpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/AuthOpenIDConnect.pm @@ -18,27 +18,7 @@ our $VERSION = '2.00'; sub authInit { my $self = shift; - # Retrieve OpenID Connect configuration data - if ( $self->{OIDCRPConfigurationURI} ) { - - my $oidcConfData = - $self->getConfigurationData( $self->{OIDCRPConfigurationURI} ); - - if ($oidcConfData) { - $self->{OIDCRPAuthorizeURI} = - $oidcConfData->{authorization_endpoint}; - $self->{OIDCRPAccessTokenURI} = $oidcConfData->{token_endpoint}; - $self->{OIDCRPUserInfoURI} = $oidcConfData->{userinfo_endpoint}; - $self->{OIDCRPJwksURI} = $oidcConfData->{jwks_uri}; - } - - } - - # Retrieve JWKS data - if ( $self->{OIDCRPJwksURI} ) { - $self->{OIDCRPJwksData} = - $self->getConfigurationData( $self->{OIDCRPJwksURI} ); - } + return PE_ERROR unless $self->loadOPs; PE_OK; } @@ -69,26 +49,7 @@ sub extractFormInfo { my $callback_get_param = $self->{OIDCRPCallbackGetParam}; my $callback = $self->param($callback_get_param); - unless ($callback) { - - # AuthN Request - $self->lmLog( "Build OpenIDConnect AuthN Request", 'debug' ); - - # Save state - my $state = $self->storeState(qw/urldc checkLogins/); - - my $stateSession = $self->storeState(); - - # TODO Use AuthChoiceParam in redirect URL - - # Authorization Code Flow - $self->{urldc} = $self->buildAuthorizationCodeAuthnRequest($state); - - $self->lmLog( "Redirect user to " . $self->{urldc}, 'debug' ); - - return $self->_subProcess(qw(autoRedirect)); - } - else { + if ($callback) { $self->lmLog( "OpenIDConnect callback URI detected: " @@ -111,8 +72,18 @@ sub extractFormInfo { } } + # Get OpenID Provider + my $op = $self->{_oidcOPCurrent}; + + unless ($op) { + $self->lmLog( "OpenID Provider not found", 'error' ); + return PE_ERROR; + } + + $self->lmLog( "Using OpenID Provider $op", 'debug' ); + # Get access_token and id_token - my $content = $self->getAuthorizationCodeAccessToken($code); + my $content = $self->getAuthorizationCodeAccessToken( $op, $code ); return PE_ERROR unless $content; my $json = $self->decodeJSON($content); @@ -130,8 +101,10 @@ sub extractFormInfo { $self->lmLog( "ID token: $id_token", 'debug' ); # Verify JWT signature - if ( $self->{OIDCRPCheckJWTSignature} ) { - unless ( $self->verifyJWTSignature($id_token) ) { + if ( $self->{oidcOPMetaDataOptions}->{$op} + ->{oidcOPMetaDataOptionsCheckJWTSignature} ) + { + unless ( $self->verifyJWTSignature( $op, $id_token ) ) { $self->lmLog( "JWT signature verification failed", 'error' ); return PE_ERROR; } @@ -159,6 +132,54 @@ sub extractFormInfo { return PE_OK; } + # No callback, choose Provider and send authn request + my $op; + + unless ( $op = $self->param("idp") ) { + $self->lmLog( "Redirecting user to OP list", 'debug' ); + + # Control url parameter + my $urlcheck = $self->controlUrlOrigin(); + return $urlcheck unless ( $urlcheck == PE_OK ); + + # IDP list + my @list = (); + foreach ( keys %{ $self->{_oidcOPList} } ) { + push @list, + { + val => $_, + name => $self->{oidcOPMetaDataOptions}->{$_} + ->{oidcOPMetaDataOptionsDisplayName}, + }; + } + $self->{list} = \@list; + + $self->{login} = 1; + return PE_CONFIRM; + } + + # Provider is choosen + $self->lmLog( "OpenID Provider $op choosen", 'debug' ); + + $self->{_oidcOPCurrent} = $op; + + # AuthN Request + $self->lmLog( "Build OpenIDConnect AuthN Request", 'debug' ); + + # Save state + my $state = $self->storeState(qw/urldc checkLogins _oidcOPCurrent/); + + my $stateSession = $self->storeState(); + + # TODO Use AuthChoiceParam in redirect URL + + # Authorization Code Flow + $self->{urldc} = $self->buildAuthorizationCodeAuthnRequest( $op, $state ); + + $self->lmLog( "Redirect user to " . $self->{urldc}, 'debug' ); + + return $self->_subProcess(qw(autoRedirect)); + PE_OK; } diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_OpenIDConnect.pm index c588e4b57..de701b5a8 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_OpenIDConnect.pm @@ -15,6 +15,52 @@ use Crypt::OpenSSL::Bignum; use base qw(Lemonldap::NG::Portal::_Browser); our $VERSION = '2.00'; +our $oidcCache; + +BEGIN { + eval { + require threads::shared; + threads::shared::share($oidcCache); + }; +} + +## @method boolean loadOPs(boolean no_cache) +# Load OpenID Connect Providers and JWKS data +# @param no_cache Disable cache use +# @return boolean result +sub loadOPs { + my ( $self, $no_cache ) = splice @_; + + # Check cache + unless ($no_cache) { + if ( $oidcCache->{_oidcOPList} ) { + $self->lmLog( "Load OPs from cache", 'debug' ); + $self->{_oidcOPList} = $oidcCache->{_oidcOPList}; + return 1; + } + } + + # Check presence of at least one identity provider in configuration + unless ( $self->{oidcOPMetaDataJSON} + and keys %{ $self->{oidcOPMetaDataJSON} } ) + { + $self->lmLog( "No OpenID Connect Provider found in configuration", + 'warn' ); + } + + # Extract JSON data + $self->{_oidcOPList} = {}; + foreach ( keys %{ $self->{oidcOPMetaDataJSON} } ) { + $self->{_oidcOPList}->{$_}->{conf} = + $self->decodeJSON( $self->{oidcOPMetaDataJSON}->{$_} ); + $self->{_oidcOPList}->{$_}->{jwks} = + $self->decodeJSON( $self->{oidcOPMetaDataJWKS}->{$_} ); + } + + $oidcCache->{_oidcOPList} = $self->{_oidcList} unless $no_cache; + + return 1; +} ## @method String getCallbackUri() # Compute callback URI @@ -35,16 +81,20 @@ sub getCallbackUri { return $callback_uri; } -## @method String buildAuthorizationCodeAuthnRequest(String state) +## @method String buildAuthorizationCodeAuthnRequest(String op, String state) # Build Authentication Request URI for Authorization Code Flow +# @param op OpenIP Provider configuration key # @param state State # return String Authentication Request URI sub buildAuthorizationCodeAuthnRequest { - my ( $self, $state ) = splice @_; + my ( $self, $op, $state ) = splice @_; - my $authorize_uri = $self->{OIDCRPAuthorizeURI}; - my $client_id = $self->{OIDCRPClientID}; - my $scope = $self->{OIDCRPScope}; + my $authorize_uri = + $self->{_oidcOPList}->{$op}->{conf}->{authorization_endpoint}; + my $client_id = + $self->{oidcOPMetaDataOptions}->{$op}->{oidcOPMetaDataOptionsClientID}; + my $scope = + $self->{oidcOPMetaDataOptions}->{$op}->{oidcOPMetaDataOptionsScope}; my $response_type = "code"; my $redirect_uri = $self->getCallbackUri; @@ -69,18 +119,23 @@ sub buildAuthorizationCodeAuthnRequest { return $authn_uri; } -## @method String getAuthorizationCodeAccessToken(String code) +## @method String getAuthorizationCodeAccessToken(String op, String code) # Get Token response with autorization code +# @param op OpenIP Provider configuration key # @param code Code # return String Token response decoded content sub getAuthorizationCodeAccessToken { - my ( $self, $code ) = splice @_; + my ( $self, $op, $code ) = splice @_; - my $client_id = $self->{OIDCRPClientID}; - my $client_secret = $self->{OIDCRPClientSecret}; - my $redirect_uri = $self->getCallbackUri; - my $access_token_uri = $self->{OIDCRPAccessTokenURI}; - my $grant_type = "authorization_code"; + my $client_id = + $self->{oidcOPMetaDataOptions}->{$op}->{oidcOPMetaDataOptionsClientID}; + my $client_secret = + $self->{oidcOPMetaDataOptions}->{$op} + ->{oidcOPMetaDataOptionsClientSecret}; + my $redirect_uri = $self->getCallbackUri; + my $access_token_uri = + $self->{_oidcOPList}->{$op}->{conf}->{token_endpoint}; + my $grant_type = "authorization_code"; my %form; $form{"code"} = $code; @@ -236,12 +291,13 @@ sub extractJWT { return \@jwt_parts; } -## @method boolean verifyJWTSignature(String jwt) +## @method boolean verifyJWTSignature(String op, String jwt) # Check signature of a JWT +# @param op OpenIP Provider configuration key # @param jwt JWT raw value # @return boolean 1 if signature is verified, 0 else sub verifyJWTSignature { - my ( $self, $jwt ) = splice @_; + my ( $self, $op, $jwt ) = splice @_; $self->lmLog( "Verification of JWT signature: $jwt", 'debug' ); @@ -276,7 +332,9 @@ sub verifyJWTSignature { if ( $alg eq "HS256" or $alg eq "HS384" or $alg eq "HS512" ) { # Check signature with client secret - my $client_secret = $self->{OIDCRPClientSecret}; + my $client_secret = + $self->{oidcOPMetaDataOptions}->{$op} + ->{oidcOPMetaDataOptionsClientSecret}; my $digest; @@ -314,13 +372,13 @@ sub verifyJWTSignature { if ( $alg eq "RS256" or $alg eq "RS384" or $alg eq "RS512" ) { # The public key is needed - unless ( $self->{OIDCRPJwksData} ) { + unless ( $self->{_oidcOPList}->{$op}->{jwks} ) { $self->lmLog( "Cannot verify $alg signature: no JWKS data found", 'error' ); return 0; } - my $keys = $self->{OIDCRPJwksData}->{keys}; + my $keys = $self->{_oidcOPList}->{$op}->{jwks}->{keys}; my $key_hash; # Find Key ID associated with signature @@ -383,31 +441,6 @@ sub verifyJWTSignature { return 0; } -## @method HashRef getConfigurationData(String confUri) -# Retrieve OpenID Connect configuration data -# @param confURI Configuration end point -# @result HashRef data -sub getConfigurationData { - my ( $self, $uri ) = splice @_; - - unless ($uri) { - $self->lmLog( "No URI given to retrieve configuration data", 'error' ); - return; - } - - $self->lmLog( "Retrieving configuration from $uri", 'debug' ); - - my $response = $self->ua->get($uri); - - if ( $response->is_error ) { - $self->lmLog( "Bad response: " . $response->message, "error" ); - $self->lmLog( $response->content, 'debug' ); - return; - } - - return $self->decodeJSON( $response->decoded_content ); -} - 1; __END__ @@ -429,6 +462,10 @@ and user information loading =head1 METHODS +=head2 loadOPs + +Load OpenID Connect Providers and JWKS data + =head2 getCallbackUri Compute callback URI @@ -465,10 +502,6 @@ Extract parts of a JWT Check signature of a JWT -=head2 getConfigurationData - -Retrieve OpenID Connect configuration data - =head1 SEE ALSO L, L diff --git a/lemonldap-ng-portal/t/70-Lemonldap-NG-Portal-AuthOpenIDConnect.t b/lemonldap-ng-portal/t/70-Lemonldap-NG-Portal-AuthOpenIDConnect.t index aba95d0b5..5afd4f777 100644 --- a/lemonldap-ng-portal/t/70-Lemonldap-NG-Portal-AuthOpenIDConnect.t +++ b/lemonldap-ng-portal/t/70-Lemonldap-NG-Portal-AuthOpenIDConnect.t @@ -30,27 +30,26 @@ ok( ## JWT Signature verification # Samples from http://jwt.io - -$p->{OIDCRPClientSecret} = "secret"; +$p->{oidcOPMetaDataOptions}->{jwtio}->{oidcOPMetaDataOptionsClientSecret} = "secret"; my $jwt; # alg: none $jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ."; -ok( $p->verifyJWTSignature($jwt) == 1, 'JWT Signature verification - alg: none'); +ok( $p->verifyJWTSignature("jwtio", $jwt) == 1, 'JWT Signature verification - alg: none'); # alg: HS256 $jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ.eoaDVGTClRdfxUZXiPs3f8FmJDkDE_VCQFXqKxpLsts"; -ok( $p->verifyJWTSignature($jwt) == 1, 'JWT Signature verification - alg: HS256'); +ok( $p->verifyJWTSignature("jwtio", $jwt) == 1, 'JWT Signature verification - alg: HS256'); # alg: HS512 $jwt = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ.fSCfxDB4cFVvzd6IqiNTuItTYiv-tAp5u5XplJWRDBGNF1rgGn1gyYK9LuHobWWpwqCzI7pEHDlyrbNHaQJmqg"; -ok( $p->verifyJWTSignature($jwt) == 1, 'JWT Signature verification - alg: HS512'); +ok( $p->verifyJWTSignature("jwtio", $jwt) == 1, 'JWT Signature verification - alg: HS512'); # Sample from Google -$p->{OIDCRPJwksData}->{keys}->[0]->{kid} = "3d007677fec656a562826f0191d0f9fcb0e595cf"; -$p->{OIDCRPJwksData}->{keys}->[0]->{n} = "3I_zvpLMNY9UY-SoVm60yh3CRB0LK0CdJ7qqF_Fl07LWNrWSudWSv1q-1QQGwQyxjzuD31eOouqp6gsMgJg6kyECUj9i6zUETCePy3kc-CAPUZE4vj-sJGA0qIcIrI54RdsLL6u27TKAkqqdl-XeO0S5fcUb3AaGW8TpmZoioEU="; -$p->{OIDCRPJwksData}->{keys}->[0]->{e} = "AQAB"; +$p->{_oidcOPList}->{google}->{jwks}->{keys}->[0]->{kid} = "3d007677fec656a562826f0191d0f9fcb0e595cf"; +$p->{_oidcOPList}->{google}->{jwks}->{keys}->[0]->{n} = "3I_zvpLMNY9UY-SoVm60yh3CRB0LK0CdJ7qqF_Fl07LWNrWSudWSv1q-1QQGwQyxjzuD31eOouqp6gsMgJg6kyECUj9i6zUETCePy3kc-CAPUZE4vj-sJGA0qIcIrI54RdsLL6u27TKAkqqdl-XeO0S5fcUb3AaGW8TpmZoioEU="; +$p->{_oidcOPList}->{google}->{jwks}->{keys}->[0]->{e} = "AQAB"; # alg: RS256 $jwt ="eyJhbGciOiJSUzI1NiIsImtpZCI6IjNkMDA3Njc3ZmVjNjU2YTU2MjgyNmYwMTkxZDBmOWZjYjBlNTk1Y2YifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTE1MzYxMjMwMzU3MzA0NzU0ODQ0IiwiYXpwIjoiMjg2MzA1NzI4NjUyLWxjYW5ubWRnMTdxM2VtdDFjYmtqbmZnOTVzZHM4NjJsLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiZW1haWwiOiJjbGVtZW50QG9vZG8ubmV0IiwiYXRfaGFzaCI6ImZRc0FaSHdsUUNPZXctNE84QkFWNWciLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiMjg2MzA1NzI4NjUyLWxjYW5ubWRnMTdxM2VtdDFjYmtqbmZnOTVzZHM4NjJsLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaGQiOiJvb2RvLm5ldCIsImlhdCI6MTQxNjQwNjA0MywiZXhwIjoxNDE2NDA5OTQzfQ.NihX-7P1ogpPCmygD-A-hChIwMg9hJQ_4gzu3zmNEyHnY9rWuwXF6E2K9LF_opMQXWJxkUcI7eyo73L3yk9_51CfQLzD5NbfpR6kyctLBXud9A7wyHzJRBCB_rOU12vU4bMWGajgkGUqOmy-PFnz3akvqVgExbqas0Go4Flg7NI"; -ok( $p->verifyJWTSignature($jwt) == 1, 'JWT Signature verification - alg: RS256'); +ok( $p->verifyJWTSignature('google', $jwt) == 1, 'JWT Signature verification - alg: RS256');