Commit 438289b4 authored by Yadd's avatar Yadd
Browse files

OIDC written (not tested) (#595)

parent ef8f8e59
......@@ -217,7 +217,7 @@ sub run {
);
$self->info( $req,
"<p><a href=\"$logout_url\">$logout_url</a></p>" );
$self->{activeTimer} = 0;
$req->datas->{activeTimer} = 0;
return PE_CONFIRM;
}
......
......@@ -3,7 +3,9 @@ package Lemonldap::NG::Portal::Issuer::OpenIDConnect;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_CONFIRM
PE_ERROR
PE_LOGOUT_OK
PE_OK
);
......@@ -12,10 +14,21 @@ our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Portal::Main::Issuer',
'Lemonldap::NG::Portal::Lib::OpenIDConnect';
# PROPERTIES
# INITIALIZATION
# OIDC has 7 endpoints managed here as PSGI endpoints or in run() [Main/Issuer.pm
# manage transparent authentication for run()]:
# - authorize : in run()
# - logout : in run()
# => endSessionDone() for unauth users
# - checksession: => checkSession() for all
# - token : => token() for unauth users (RP)
# - userinfo : => userInfo() for unauth users (RP)
# - jwks : => jwks() for unauth users (RP)
# - register : => registration() for unauth users (RP)
#
# Other paths will be handle by run() and return PE_ERROR
sub init {
my ($self) = @_;
......@@ -27,43 +40,818 @@ sub init {
# Manage RP requests
$self->addRouteFromConf(
'Unauth',
oidcServiceMetaDataEndSessionURI => 'endSessionDone',
oidcServiceMetaDataCheckSessionURI => 'checkSession',
oidcServiceMetaDataTokenURI => 'token',
oidcServiceMetaDataUserInfoURI => 'userInfo',
oidcServiceMetaDataJWKSURI => 'jwks',
oidcServiceMetaDataRegistrationURI => 'registration',
oidcServiceMetaDataEndSessionURI => 'endSessionDone',
oidcServiceMetaDataCheckSessionURI => 'checkSession',
);
# Manage user requests
$self->addRouteFromConf(
'Auth',
oidcServiceMetaDataCheckSessionURI => 'checkSession',
oidcServiceMetaDataTokenURI => 'badAuthRequest',
oidcServiceMetaDataUserInfoURI => 'badAuthRequest',
oidcServiceMetaDataJWKSURI => 'badAuthRequest',
oidcServiceMetaDataRegistrationURI => 'badAuthRequest',
oidcServiceMetaDataCheckSessionURI => 'checkSession',
);
return 1;
}
# PROPERTIES
has '_confAcc' => ( is => 'rw', isa => 'Lemonldap::NG::Common::Conf' );
# Configuration access object
# Return _confAcc property if exists or create it. Used for RP registration
#@return Lemonldap::NG::Common::Conf object
sub confAcc {
my $self = shift;
return $self->_confAcc if ( $self->_confAcc );
# TODO: pass args and remove this
my $d = `pwd`;
chomp $d;
my $tmp;
unless ( $tmp = Lemonldap::NG::Common::Conf->new( $self->configStorage ) ) {
die "Unable to build Lemonldap::NG::Common::Conf "
. $Lemonldap::NG::Common::Conf::msg;
}
return $self->_confAcc($tmp);
}
# RUNNING METHODS
# Main method (launched only for authenticated users, see Main/Issuer)
# Main method (launched only for authenticated users, see Main/Issuer.pm)
# run() manages only "authorize" and "logout" endpoints.
sub run {
my ( $self, $req, $path ) = @_;
if ($path) {
# AUTHORIZE
if ( $path eq $self->conf->{oidcServiceMetaDataAuthorizeURI} ) {
$self->lmLog( "URL detected as an OpenID Connect AUTHORIZE URL",
'debug' );
# Get and save parameters
my $oidc_request = {};
foreach my $param (
qw/response_type scope client_id state redirect_uri nonce
response_mode display prompt max_age ui_locales id_token_hint
login_hint acr_valuesi request request_uri/
)
{
$oidc_request->{$param} = $req->param($param);
$self->lmLog(
"OIDC request parameter $param: " . $oidc_request->{$param},
'debug'
);
}
# Detect requested flow
my $response_type = $oidc_request->{'response_type'};
my $flow = $self->getFlowType($response_type);
unless ($flow) {
$self->lmLog( "Unknown response type: $response_type",
'error' );
return PE_ERROR;
}
$self->lmLog(
"OIDC $flow flow requested (response type: $response_type)",
'debug' );
# Extract request_uri/request parameter
if ( $oidc_request->{'request_uri'} ) {
my $request =
$self->getRequestJWT( $oidc_request->{'request_uri'} );
if ($request) {
$oidc_request->{'request'} = $request;
}
else {
$self->lmLog( "Error with Request URI resolution",
'error' );
return PE_ERROR;
}
}
if ( $oidc_request->{'request'} ) {
my $request =
$self->getJWTJSONData( $oidc_request->{'request'} );
# Override OIDC parameters by request content
foreach ( keys %$request ) {
$self->lmLog(
"Override $_ OIDC param by value present in request parameter",
'debug'
);
$oidc_request->{$_} = $request->{$_};
$self->setHiddenFormValue( $_, $request->{$_}, '' );
}
}
# Check all required parameters
unless ( $oidc_request->{'redirect_uri'} ) {
$self->lmLog( "Redirect URI is required", 'error' );
return PE_ERROR;
}
unless ( $oidc_request->{'scope'} ) {
$self->lmLog( "Scope is required", 'error' );
$self->returnRedirectError(
$req,
$oidc_request->{'redirect_uri'},
"invalid_request",
"scope required",
undef,
$oidc_request->{'state'},
( $flow ne "authorizationcode" )
);
}
unless ( $oidc_request->{'client_id'} ) {
$self->lmLog( "Client ID is required", 'error' );
return $self->returnRedirectError(
$req,
$oidc_request->{'redirect_uri'},
"invalid_request",
"client_id required",
undef,
$oidc_request->{'state'},
( $flow ne "authorizationcode" )
);
}
if ( $flow eq "implicit" and not defined $oidc_request->{'nonce'} )
{
$self->lmLog( "Nonce is required for implicit flow", 'error' );
return $self->returnRedirectError(
$req, $oidc_request->{'redirect_uri'},
"invalid_request", "nonce required",
undef, $oidc_request->{'state'}, 1
);
}
# Check if flow is allowed
if ( $flow eq "authorizationcode"
and not $self->conf->{oidcServiceAllowAuthorizationCodeFlow} )
{
$self->lmLog( "Authorization code flow is not allowed",
'error' );
return $self->returnRedirectError(
$req, $oidc_request->{'redirect_uri'},
"server_error", "Authorization code flow not allowed",
undef, $oidc_request->{'state'},
0
);
}
if ( $flow eq "implicit"
and not $self->conf->{oidcServiceAllowImplicitFlow} )
{
$self->lmLog( "Implicit flow is not allowed", 'error' );
return $self->returnRedirectError(
$req, $oidc_request->{'redirect_uri'},
"server_error", "Implicit flow not allowed",
undef, $oidc_request->{'state'},
1
);
}
if ( $flow eq "hybrid"
and not $self->conf->{oidcServiceAllowHybridFlow} )
{
$self->lmLog( "Hybrid flow is not allowed", 'error' );
return $self->returnRedirectError(
$req, $oidc_request->{'redirect_uri'},
"server_error", "Hybrid flow not allowed",
undef, $oidc_request->{'state'},
1
);
}
# Check if user needs to be reauthenticated
my $reauthentication = 0;
my $prompt = $oidc_request->{'prompt'};
if ( $prompt =~ /\blogin\b/ ) {
$self->lmLog(
"Reauthentication requested by Relying Party in prompt parameter",
'debug'
);
$reauthentication = 1;
}
my $max_age = $oidc_request->{'max_age'};
my $_lastAuthnUTime = $req->{sessionInfo}->{_lastAuthnUTime};
if ( $max_age && time > $_lastAuthnUTime + $max_age ) {
$self->lmLog(
"Reauthentication forced cause authentication time ($_lastAuthnUTime) is too old (>$max_age s)",
'debug'
);
$reauthentication = 1;
}
if ($reauthentication) {
# Set prompt to 0 to avoid loop
$self->setHiddenFormValue( $req, 'prompt', '', '' );
# Replay authentication process
$self->{updateSession} = 1;
$req->steps(
[
$self->p->authProcess,
@{ $self->p->betweenAuthAndDatas },
$self->p->sessionDatas,
@{ $self->p->afterDatas }
]
);
# Update session_id
return PE_OK;
}
# Check openid scope
unless ( $oidc_request->{'scope'} =~ /\bopenid\b/ ) {
$self->lmLog( "No openid scope found", 'debug' );
#TODO manage standard OAuth request
return PE_OK;
}
# Check client_id
my $client_id = $oidc_request->{'client_id'};
$self->lmLog( "Request from client id $client_id", 'debug' );
# Verify that client_id is registered in configuration
my $rp = $self->getRP($client_id);
unless ($rp) {
$self->lmLog(
"No registered Relying Party found with client_id $client_id",
'error'
);
return $self->returnRedirectError(
$req,
$oidc_request->{'redirect_uri'},
"invalid_request",
"client_id $client_id unknown",
undef,
$oidc_request->{'state'},
( $flow ne "authorizationcode" )
);
}
else {
$self->lmLog( "Client id $client_id match RP $rp", 'debug' );
}
# Check Request JWT signature
if ( $oidc_request->{'request'} ) {
unless (
$self->verifyJWTSignature(
$oidc_request->{'request'},
undef, $rp
)
)
{
$self->lmLog( "Request JWT signature could not be verified",
'error' );
return PE_ERROR;
}
else {
$self->lmLog( "Request JWT signature verified", 'debug' );
}
}
# Check redirect_uri
my $redirect_uri = $oidc_request->{'redirect_uri'};
my $redirect_uris = $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsRedirectUris};
if ($redirect_uris) {
my $redirect_uri_allowed = 0;
foreach ( split( /\s+/, $redirect_uris ) ) {
$redirect_uri_allowed = 1 if $redirect_uri eq $_;
}
unless ($redirect_uri_allowed) {
$self->lmLog( "Redirect URI $redirect_uri not allowed",
'error' );
return $self->returnRedirectError(
$req,
$oidc_request->{'redirect_uri'},
"invalid_request",
"redirect_uri $redirect_uri not allowed",
undef,
$oidc_request->{'state'},
( $flow ne "authorizationcode" )
);
}
}
# Check id_token_hint
my $id_token_hint = $oidc_request->{'id_token_hint'};
if ($id_token_hint) {
$self->lmLog( "Check sub of ID Token $id_token_hint", 'debug' );
# Check that id_token_hint sub match current user
my $sub = $self->getIDTokenSub($id_token_hint);
my $user_id_attribute =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsUserIDAttr} || $self->{whatToTrace};
my $user_id = $req->{sessionInfo}->{$user_id_attribute};
unless ( $sub eq $user_id ) {
$self->lmLog(
"ID Token hint sub $sub do not match user $user_id",
'error' );
return $self->returnRedirectError(
$req,
$oidc_request->{'redirect_uri'},
"invalid_request",
"current user do not match id_token_hint sub",
undef,
$oidc_request->{'state'},
( $flow ne "authorizationcode" )
);
}
else {
$self->lmLog( "ID Token hint sub $sub match current user",
'debug' );
}
}
# Obtain consent
my $bypassConsent = $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsBypassConsent};
if ($bypassConsent) {
$self->lmLog(
"Consent is disabled for RP $rp, user will not be prompted",
'debug'
);
}
else {
my $ask_for_consent = 1;
if ( $req->{sessionInfo}->{"_oidc_consent_time_$rp"}
and $req->{sessionInfo}->{"_oidc_consent_scope_$rp"} )
{
$ask_for_consent = 0;
my $consent_time =
$req->{sessionInfo}->{"_oidc_consent_time_$rp"};
my $consent_scope =
$req->{sessionInfo}->{"_oidc_consent_scope_$rp"};
$self->lmLog(
"Consent already given for Relying Party $rp (time: $consent_time, scope: $consent_scope)",
'debug'
);
# Check accepted scope
foreach my $requested_scope (
split( /\s+/, $oidc_request->{'scope'} ) )
{
if ( $consent_scope =~ /\b$requested_scope\b/ ) {
$self->lmLog(
"Scope $requested_scope already accepted",
'debug' );
}
else {
$self->lmLog(
"Scope $requested_scope was not previously accepted",
'debug'
);
$ask_for_consent = 1;
last;
}
}
# Check prompt parameter
$ask_for_consent = 1 if ( $prompt =~ /\bconsent\b/ );
}
if ($ask_for_consent) {
if ( $self->param('confirm') == 1 ) {
$self->updatePersistentSession(
{ "_oidc_consent_time_$rp" => time } );
$self->updatePersistentSession(
{
"_oidc_consent_scope_$rp" =>
$oidc_request->{'scope'}
}
);
$self->lmLog( "Consent given for Relying Party $rp",
'debug' );
}
elsif ( $req->param('confirm') == -1 ) {
$self->lmLog(
"User refused consent for Relying party $rp",
'debug' );
return $self->returnRedirectError(
$req,
$oidc_request->{'redirect_uri'},
"consent_required",
"consent not given",
undef,
$oidc_request->{'state'},
( $flow ne "authorizationcode" )
);
}
else {
$self->lmLog(
"Obtain user consent for Relying Party $rp",
'debug' );
# Return error if prompt is none
if ( $prompt =~ /\bnone\b/ ) {
$self->lmLog(
"Consent is needed but prompt is none",
'debug' );
return $self->returnRedirectError(
$req,
$oidc_request->{'redirect_uri'},
"consent_required",
"consent required",
undef,
$oidc_request->{'state'},
( $flow ne "authorizationcode" )
);
}
my $display_name =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsDisplayName};
my $icon = $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsIcon};
my $img_src;
if ($icon) {
$img_src =
( $icon =~ m#^https?://# )
? $icon
: $self->p->staticPrefic . "/common/" . $icon;
}
# HERE
$self->info('<div class="oidc_consent_message">');
$self->info( '<img src="' . $img_src . '" />' )
if $img_src;
$self->info(
qq'<h3 trspan="oidcConsent,$display_name">The application $display_name would like to know:</h3><ul>'
);
my $scope_messages = {
openid => 'yourIdentity',
profile => 'yourProfile',
email => 'yourEmail',
address => 'yourAddress',
phone => 'yourPhone',
};
foreach my $requested_scope (
split( /\s/, $oidc_request->{'scope'} ) )
{
my $message = $scope_messages->{$requested_scope}
|| 'anotherInformation';
$self->info(
qq'<li trspan="$message ">$message</li>');
}
$self->info('</ul></div>');
$req->datas->{activeTimer} = 0;
return PE_CONFIRM;
}
}
}
# Create session_state
my $session_state =
$self->createSessionState( $req->id, $client_id );
# Authorization Code Flow
if ( $flow eq "authorizationcode" ) {
# Generate code
my $codeSession = $self->getOpenIDConnectSession();
my $code = $codeSession->id();
$self->lmLog( "Generated code: $code", 'debug' );
# Store data in session
$codeSession->update(
{
redirect_uri => $oidc_request->{'redirect_uri'},
scope => $oidc_request->{'scope'},
user_session_id => $req->id,
_utime => time,
nonce => $oidc_request->{'nonce'},
}
);
# Build Response
my $response_url = $self->buildAuthorizationCodeAuthnResponse(
$oidc_request->{'redirect_uri'},
$code, $oidc_request->{'state'},
$session_state
);
$self->lmLog( "Redirect user to $response_url", 'debug' );
$req->urldc($response_url);
$req->steps( [] );
return PE_OK;
}
# Implicit Flow
if ( $flow eq "implicit" ) {
my $access_token;
my $at_hash;
if ( $response_type =~ /\btoken\b/ ) {
# Generate access_token
my $accessTokenSession = $self->getOpenIDConnectSession;
unless ($accessTokenSession) {
$self->lmLog(
"Unable to create OIDC session for access_token",
"error" );
$self->returnRedirectError( $req,
$oidc_request->{'redirect_uri'},
"server_error", undef, undef,
$oidc_request->{'state'}, 1 );
}