lemonldap-ng/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_OpenIDConnect.pm

584 lines
15 KiB
Perl
Raw Normal View History

2014-11-14 17:18:50 +01:00
## @file
# Common OpenID Connect functions
## @class
# Common OpenID Connect functions
package Lemonldap::NG::Portal::_OpenIDConnect;
use strict;
use JSON;
use MIME::Base64 qw/decode_base64url/;
2014-11-14 17:18:50 +01:00
use URI::Escape;
use Digest::SHA qw/hmac_sha256_base64 hmac_sha384_base64 hmac_sha512_base64/;
use Crypt::OpenSSL::RSA;
use Crypt::OpenSSL::Bignum;
2014-11-14 17:18:50 +01:00
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;
}
2014-11-14 17:18:50 +01:00
## @method String getCallbackUri()
# Compute callback URI
# @return String Callback URI
sub getCallbackUri {
my $self = shift;
my $callback_get_param = $self->{OIDCRPCallbackGetParam};
my $callback_uri = $self->{portal};
$callback_uri .=
( $self->{portal} =~ /\?/ )
? '&' . $callback_get_param . '=1'
: '?' . $callback_get_param . '=1';
$self->lmLog( "OpenIDConnect Callback URI: $callback_uri", 'debug' );
return $callback_uri;
}
## @method String buildAuthorizationCodeAuthnRequest(String op, String state)
2014-11-14 17:18:50 +01:00
# Build Authentication Request URI for Authorization Code Flow
# @param op OpenIP Provider configuration key
2014-11-14 17:18:50 +01:00
# @param state State
# return String Authentication Request URI
sub buildAuthorizationCodeAuthnRequest {
my ( $self, $op, $state ) = splice @_;
my $authorize_uri =
$self->{_oidcOPList}->{$op}->{conf}->{authorization_endpoint};
my $client_id =
$self->{oidcOPMetaDataOptions}->{$op}->{oidcOPMetaDataOptionsClientID};
my $scope =
$self->{oidcOPMetaDataOptions}->{$op}->{oidcOPMetaDataOptionsScope};
2014-11-14 17:18:50 +01:00
my $response_type = "code";
my $redirect_uri = $self->getCallbackUri;
$client_id = uri_escape($client_id);
$scope = uri_escape($scope);
$response_type = uri_escape($response_type);
$redirect_uri = uri_escape($redirect_uri);
$state = uri_escape($state) if defined $state;
my $authn_uri = $authorize_uri;
$authn_uri .= ( $authorize_uri =~ /\?/ ? '&' : '?' );
$authn_uri .= "response_type=$response_type";
$authn_uri .= "&client_id=$client_id";
$authn_uri .= "&scope=$scope";
$authn_uri .= "&redirect_uri=$redirect_uri";
$authn_uri .= "&state=$state" if defined $state;
$self->lmLog(
"OpenIDConnect Authorization Code Flow Authn Request: $authn_uri",
'debug' );
return $authn_uri;
}
## @method String getAuthorizationCodeAccessToken(String op, String code)
2014-11-14 17:18:50 +01:00
# Get Token response with autorization code
# @param op OpenIP Provider configuration key
2014-11-14 17:18:50 +01:00
# @param code Code
# return String Token response decoded content
sub getAuthorizationCodeAccessToken {
my ( $self, $op, $code ) = splice @_;
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";
2014-11-14 17:18:50 +01:00
my %form;
$form{"code"} = $code;
$form{"client_id"} = $client_id;
$form{"client_secret"} = $client_secret;
$form{"redirect_uri"} = $redirect_uri;
$form{"grant_type"} = $grant_type;
my $response = $self->ua->post( $access_token_uri, \%form,
"Content-Type" => 'application/x-www-form-urlencoded' );
if ( $response->is_error ) {
$self->lmLog( "Bad authorization response: " . $response->message,
"error" );
$self->lmLog( $response->content, 'debug' );
return 0;
}
return $response->decoded_content;
}
2014-11-20 16:53:26 +01:00
## @method String getUserInfo(String op, String access_token)
# Get UserInfo response
# @param op OpenIP Provider configuration key
# @param access_token Access Token
# return String UserInfo response decoded content
sub getUserInfo {
my ( $self, $op, $access_token ) = splice @_;
my $userinfo_uri = $self->{_oidcOPList}->{$op}->{conf}->{userinfo_endpoint};
unless ($userinfo_uri) {
$self->lmLog( "UserInfo URI not found in $op configuration", 'error' );
return 0;
}
$self->lmLog(
"Request User Info on $userinfo_uri with access token $access_token",
'debug' );
my $response = $self->ua->get( $userinfo_uri,
"Authorization" => "Bearer $access_token" );
if ( $response->is_error ) {
$self->lmLog( "Bad userinfo response: " . $response->message, "error" );
$self->lmLog( $response->content, 'debug' );
return 0;
}
return $response->decoded_content;
}
2014-11-14 17:53:56 +01:00
## @method HashRef decodeJSON(String json)
# Convert JSON to HashRef
# @param json JSON raw content
# @return HashRef JSON decoded content
sub decodeJSON {
my ( $self, $json ) = splice @_;
my $json_hash;
eval { $json_hash = decode_json $json; };
if ($@) {
$json_hash->{error} = "parse_error";
}
return $json_hash;
}
## @method hashref getOpenIDConnectSession(string id)
# Try to recover the OpenID Connect session corresponding to id and return session
# If id is set to undef, return a new session
# @param id session reference
# @return Lemonldap::NG::Common::Session object
sub getOpenIDConnectSession {
my ( $self, $id ) = splice @_;
my $oidcSession = Lemonldap::NG::Common::Session->new(
{
storageModule => $self->{globalStorage},
storageModuleOptions => $self->{globalStorageOptions},
cacheModule => $self->{localSessionStorage},
cacheModuleOptions => $self->{localSessionStorageOptions},
id => $id,
kind => "OpenIDConnect",
}
);
if ( $oidcSession->error ) {
if ($id) {
$self->_sub( 'userInfo',
"OpenIDConnect session $id isn't yet available" );
}
else {
$self->lmLog( "Unable to create new OpenIDConnect session",
'error' );
$self->lmLog( $oidcSession->error, 'error' );
}
return undef;
}
return $oidcSession;
}
## @method string storeState(array data)
# Store information in state database and return
# corresponding session_id
# @param data Array of information to store
# @return State Session ID
sub storeState {
my ( $self, @data ) = splice @_;
# check if there are data to store
my $infos;
foreach (@data) {
$infos->{$_} = $self->{$_} if $self->{$_};
}
return unless ($infos);
# Create state session
my $stateSession = $self->getOpenIDConnectSession();
return unless $stateSession;
# Session type
$infos->{_type} = "state";
# Set _utime for session autoremove
# Use default session timeout and relayState session timeout to compute it
my $time = time();
my $timeout = $self->{timeout};
my $stateTimeout = $self->{OIDCRPStateTimeout} || $timeout;
$infos->{_utime} = $time + ( $stateTimeout - $timeout );
# Store infos in state session
$stateSession->update($infos);
# Return session ID
return $stateSession->id;
}
## @method boolean extractState(string state)
# Extract state information into $self
# @param state state value
# @return result
sub extractState {
my ( $self, $state ) = splice @_;
return 0 unless $state;
# Open state session
my $stateSession = $self->getOpenIDConnectSession($state);
return 0 unless $stateSession;
# Push values in $self
foreach ( keys %{ $stateSession->data } ) {
next if $_ =~ /(type|_session_id|_utime)/;
$self->{$_} = $stateSession->data->{$_};
}
# Delete state session
if ( $stateSession->remove ) {
$self->lmLog( "State $state was deleted", 'debug' );
}
else {
$self->lmLog( "Unable to delete state $state", 'error' );
$self->lmLog( $stateSession->error, 'error' );
}
return 1;
}
## @method arrayref extractJWT(String jwt)
# Extract parts of a JWT
# @param jwt JWT raw value
# @return arrayref JWT parts
sub extractJWT {
my ( $self, $jwt ) = splice @_;
my @jwt_parts = split( /\./, $jwt );
return \@jwt_parts;
}
## @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, $op, $jwt ) = splice @_;
$self->lmLog( "Verification of JWT signature: $jwt", 'debug' );
# Extract JWT parts
my $jwt_parts = $self->extractJWT($jwt);
# Read header
my $jwt_header_part = $jwt_parts->[0];
my $jwt_header_hash =
$self->decodeJSON( decode_base64url($jwt_header_part) );
# Get signature algorithm
my $alg = $jwt_header_hash->{alg};
$self->lmLog( "JWT signature algorithm: $alg", 'debug' );
if ( $alg eq "none" ) {
# If none alg, signature should be empty
if ( $jwt_parts->[2] ) {
$self->lmLog(
"Signature "
. $jwt_parts->[2]
. " is present but algorithm is 'none'",
'debug'
);
return 0;
}
return 1;
}
if ( $alg eq "HS256" or $alg eq "HS384" or $alg eq "HS512" ) {
# Check signature with client secret
my $client_secret =
$self->{oidcOPMetaDataOptions}->{$op}
->{oidcOPMetaDataOptionsClientSecret};
my $digest;
if ( $alg eq "HS256" ) {
$digest =
hmac_sha256_base64( $jwt_parts->[0] . "." . $jwt_parts->[1],
$client_secret );
}
if ( $alg eq "HS384" ) {
$digest =
hmac_sha384_base64( $jwt_parts->[0] . "." . $jwt_parts->[1],
$client_secret );
}
if ( $alg eq "HS512" ) {
$digest =
hmac_sha512_base64( $jwt_parts->[0] . "." . $jwt_parts->[1],
$client_secret );
}
# Convert + and / to get Base64 URL valid (RFC 4648)
$digest =~ s/\+/-/g;
$digest =~ s/\//_/g;
unless ( $digest eq $jwt_parts->[2] ) {
$self->lmLog(
"Digest $digest not equal to signature " . $jwt_parts->[2],
'debug' );
return 0;
}
return 1;
}
if ( $alg eq "RS256" or $alg eq "RS384" or $alg eq "RS512" ) {
# The public key is needed
unless ( $self->{_oidcOPList}->{$op}->{jwks} ) {
$self->lmLog( "Cannot verify $alg signature: no JWKS data found",
'error' );
return 0;
}
my $keys = $self->{_oidcOPList}->{$op}->{jwks}->{keys};
my $key_hash;
# Find Key ID associated with signature
my $kid = $jwt_header_hash->{kid};
if ($kid) {
$self->lmLog( "Search key with id $kid", 'debug' );
foreach (@$keys) {
if ( $_->{kid} eq $kid ) {
$key_hash = $_;
last;
}
}
}
else {
$key_hash = shift @$keys;
}
unless ($key_hash) {
$self->lmLog( "No key found in JKWS data", 'error' );
return 0;
}
$self->lmLog( "Found public key parameter n: " . $key_hash->{n},
'debug' );
$self->lmLog( "Found public key parameter e: " . $key_hash->{e},
'debug' );
# Create public key
my $n =
Crypt::OpenSSL::Bignum->new_from_bin(
decode_base64url( $key_hash->{n} ) );
my $e =
Crypt::OpenSSL::Bignum->new_from_bin(
decode_base64url( $key_hash->{e} ) );
my $public_key = Crypt::OpenSSL::RSA->new_key_from_parameters( $n, $e );
if ( $alg eq "RS256" ) {
$public_key->use_sha256_hash;
}
if ( $alg eq "RS384" ) {
$public_key->use_sha384_hash;
}
if ( $alg eq "RS512" ) {
$public_key->use_sha512_hash;
}
return $public_key->verify(
$jwt_parts->[0] . "." . $jwt_parts->[1],
decode_base64url( $jwt_parts->[2] )
);
}
# Other algorithms not managed
$self->lmLog( "Algorithm $alg not known", 'debug' );
return 0;
}
2014-11-14 17:18:50 +01:00
1;
__END__
=head1 NAME
=encoding utf8
Lemonldap::NG::Portal::_OpenIDConnect - Common OpenIDConnect functions
=head1 SYNOPSIS
use Lemonldap::NG::Portal::_OpenIDConnect;
=head1 DESCRIPTION
This module contains common methods for OpenIDConnect authentication
and user information loading
=head1 METHODS
=head2 loadOPs
Load OpenID Connect Providers and JWKS data
2014-11-14 17:18:50 +01:00
=head2 getCallbackUri
Compute callback URI
=head2 buildAuthorizationCodeAuthnRequest
Build Authentication Request URI for Authorization Code Flow
=head2 getAuthorizationCodeAccessToken
Get Token response with autorization code
2014-11-20 16:53:26 +01:00
=head2 getUserInfo
Get UserInfo response
2014-11-14 17:53:56 +01:00
=head2 decodeJSON
Convert JSON to HashRef
=head2 getOpenIDConnectSession
Try to recover the OpenID Connect session corresponding to id and return session
=head2 storeState
Store information in state database and return
=head2 extractState
Extract state information into $self
=head2 extractJWT
Extract parts of a JWT
=head2 verifyJWTSignature
Check signature of a JWT
2014-11-14 17:18:50 +01:00
=head1 SEE ALSO
L<Lemonldap::NG::Portal::AuthOpenIDConnect>, L<Lemonldap::NG::Portal::UserDBOpenIDConnect>
=head1 AUTHOR
=over
=item Clement Oudot, E<lt>clem.oudot@gmail.comE<gt>
=back
=head1 BUG REPORT
Use OW2 system to report bug or ask for features:
L<http://jira.ow2.org>
=head1 DOWNLOAD
Lemonldap::NG is available at
L<http://forge.objectweb.org/project/showfiles.php?group_id=274>
=head1 COPYRIGHT AND LICENSE
=over
=item Copyright (C) 2014 by Clement Oudot, E<lt>clem.oudot@gmail.comE<gt>
=back
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see L<http://www.gnu.org/licenses/>.
=cut