Change configuration format to allow to define several OP (#183)

This commit is contained in:
Clément Oudot 2014-11-20 14:03:32 +00:00
parent 74a7770fa4
commit 687f0ed094
3 changed files with 152 additions and 99 deletions

View File

@ -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;
}

View File

@ -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>

View File

@ -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');