Change configuration format to allow to define several OP (#183)
This commit is contained in:
parent
74a7770fa4
commit
687f0ed094
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Lemonldap::NG::Portal::AuthOpenIDConnect>, L<Lemonldap::NG::Portal::UserDBOpenIDConnect>
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue
Block a user