From cdbe7d89e9803fece9c44859a1d1f2c9ddb14e5f Mon Sep 17 00:00:00 2001 From: Xavier Guimard Date: Sun, 1 Jan 2017 09:43:48 +0000 Subject: [PATCH] OIDC in progress (#595) --- TODO-2.0.md | 9 +- .../Lemonldap/NG/Portal/Auth/OpenIDConnect.pm | 72 ++++++-------- .../NG/Portal/Issuer/OpenIDConnect.pm | 54 ++++++---- .../Lemonldap/NG/Portal/Lib/OpenIDConnect.pm | 23 +++-- .../lib/Lemonldap/NG/Portal/Main/Display.pm | 87 +++++++--------- .../lib/Lemonldap/NG/Portal/Main/Issuer.pm | 4 +- .../lib/Lemonldap/NG/Portal/Main/Process.pm | 21 +++- .../lib/Lemonldap/NG/Portal/Main/Request.pm | 3 - .../NG/Portal/UserDB/OpenIDConnect.pm | 1 + .../t/32-Auth-and-issuer-OIDC.t | 98 ++++++++++++++++++- lemonldap-ng-portal/t/lmConf-1.js | 6 ++ lemonldap-ng-portal/t/test-lib.pm | 26 +++-- 12 files changed, 260 insertions(+), 144 deletions(-) diff --git a/TODO-2.0.md b/TODO-2.0.md index 6c1b3a87d..d6d883287 100644 --- a/TODO-2.0.md +++ b/TODO-2.0.md @@ -1,9 +1,6 @@ -# Missing methods - -* info() - -# Other - +* checkLogins in SAML +* 2nd arg for trspan +* verify activeTimer on/off on screen * Verify `useSafeJail=0` * Finish IssuerGet logout (-> info()) * Import r5420 (ssl\_opts) to trunk diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm index 83d8a147a..362a28b56 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm @@ -2,6 +2,7 @@ package Lemonldap::NG::Portal::Auth::OpenIDConnect; use strict; use Mouse; +use MIME::Base64 qw/encode_base64 decode_base64/; use Lemonldap::NG::Portal::Main::Constants qw( PE_CONFIRM PE_ERROR @@ -15,7 +16,7 @@ extends 'Lemonldap::NG::Portal::Auth::Base', # INTERFACE -has oplist => ( is => 'rw', default => sub { [] } ); +has opList => ( is => 'rw', default => sub { [] } ); has opNumber => ( is => 'rw', default => 0 ); # INITIALIZATION @@ -56,7 +57,7 @@ sub init { class => "openidconnect", }; } - $self->oplist( [@list] ); + $self->opList( [@list] ); return 1; } @@ -68,15 +69,15 @@ sub extractFormInfo { # Check callback if ( $req->param( $self->conf->{oidcRPCallbackGetParam} ) ) { - $self->lmLog( - 'OpenIDConnect callback URI detected: ' . $req->uri, 'debug' ); + $self->lmLog( 'OpenIDConnect callback URI detected: ' . $req->uri, + 'debug' ); # AuthN Response my $state = $req->param('state'); # Restore state if ($state) { - if ( $self->extractState($req, $state) ) { + if ( $self->extractState( $req, $state ) ) { $self->lmLog( "State $state extracted", 'debug' ); } else { @@ -86,8 +87,6 @@ sub extractFormInfo { } # Get OpenID Provider - $self->lmLog( "################### TODO: verify this (Auth:52)", - 'debug' ); my $op = $req->datas->{_oidcOPCurrent}; unless ($op) { @@ -115,7 +114,8 @@ sub extractFormInfo { my $code = $req->param("code"); my $auth_method = $self->conf->{oidcOPMetaDataOptions}->{$op} - ->{oidcOPMetaDataOptionsTokenEndpointAuthMethod}; + ->{oidcOPMetaDataOptionsTokenEndpointAuthMethod} + || 'client_secret_post'; my $content = $self->getAuthorizationCodeAccessToken( $req, $op, $code, @@ -197,7 +197,7 @@ sub extractFormInfo { $req->datas->{id_token} = $id_token; $self->lmLog( "Found user_id: " . $user_id, 'debug' ); - $self->{user} = $user_id; + $req->user($user_id); return PE_OK; } @@ -210,7 +210,7 @@ sub extractFormInfo { # Auto select provider if there is only one if ( $self->opNumber == 1 ) { - $op = $self->opList->[0]; + $op = $self->opList->[0]->{val}; $self->lmLog( "Selecting the only defined OP: $op", 'debug' ); } @@ -222,29 +222,7 @@ sub extractFormInfo { my $portalPath = $self->{portal}; $portalPath =~ s#^https?://[^/]+/?#/#; - foreach ( @{ $self->oplist } ) { - my $name = $self->conf->{oidcOPMetaDataOptions}->{$_} - ->{oidcOPMetaDataOptionsDisplayName}; - my $icon = $self->conf->{oidcOPMetaDataOptions}->{$_} - ->{oidcOPMetaDataOptionsIcon}; - my $img_src; - - if ($icon) { - $img_src = - ( $icon =~ m#^https?://# ) - ? $icon - : $portalPath . "skins/common/" . $icon; - } - - push @list, - { - val => $_, - name => $name, - icon => $img_src, - class => "openidconnect", - }; - } - $req->datas->{list} = $self->opList; + $req->datas->{list} = $self->opList; $req->datas->{confirmRemember} = 0; $req->datas->{login} = 1; @@ -261,14 +239,15 @@ sub extractFormInfo { $self->lmLog( "Build OpenIDConnect AuthN Request", 'debug' ); # Save state - my $state = $self->storeState(qw/urldc checkLogins _oidcOPCurrent/); + my $state = $self->storeState( $req, qw/urldc checkLogins _oidcOPCurrent/ ); # Authorization Code Flow - $req->urldc( $self->buildAuthorizationCodeAuthnRequest( $req, $op, $state ) ); + $req->urldc( + $self->buildAuthorizationCodeAuthnRequest( $req, $op, $state ) ); - $self->lmLog( "Redirect user to " . $self->{urldc}, 'debug' ); - - $req->steps([]); + $self->lmLog( "Redirect user to " . $req->{urldc}, 'debug' ); + $req->continue(1); + $req->steps( [] ); return PE_OK; } @@ -279,15 +258,17 @@ sub authenticate { sub setAuthSessionInfo { my ( $self, $req ) = @_; - my $op = $req->datas->{_oidcOPCurrent}; + my $op = $req->datas->{_oidcOPCurrent}; $req->{sessionInfo}->{authenticationLevel} = $self->conf->{oidcAuthnLevel}; $req->{sessionInfo}->{OpenIDConnect_OP} = $op; - $req->{sessionInfo}->{OpenIDConnect_access_token} = $req->datas->{access_token}; + $req->{sessionInfo}->{OpenIDConnect_access_token} = + $req->datas->{access_token}; # Keep ID Token in session - my $store_IDToken = $self->conf->{oidcOPMetaDataOptions}->{$op}->{oidcOPMetaDataOptionsStoreIDToken}; + my $store_IDToken = $self->conf->{oidcOPMetaDataOptions}->{$op} + ->{oidcOPMetaDataOptionsStoreIDToken}; if ($store_IDToken) { $self->lmLog( "Store ID Token in session", 'debug' ); $req->{sessionInfo}->{OpenIDConnect_IDToken} = $req->datas->{id_token}; @@ -311,11 +292,14 @@ sub authLogout { if ($endsession_endpoint) { my $logout_url = $self->conf->{portal} . '?logout=1'; $req->urldc( - $self->buildLogoutRequest( $endsession_endpoint, - $self->{sessionInfo}->{OpenIDConnect_IDToken}, $logout_url )); + $self->buildLogoutRequest( + $endsession_endpoint, + $self->{sessionInfo}->{OpenIDConnect_IDToken}, $logout_url + ) + ); $self->lmLog( - "OpenID Connect logout to $op will be done on ".$req->urldc, + "OpenID Connect logout to $op will be done on " . $req->urldc, 'debug' ); } else { diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm index 3a2dd2db2..aa5a0d79a 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm @@ -116,11 +116,14 @@ sub run { login_hint acr_valuesi request request_uri/ ) { - $oidc_request->{$param} = $req->param($param); - $self->lmLog( - "OIDC request parameter $param: " . $oidc_request->{$param}, - 'debug' - ); + if ( $req->param($param) ) { + $oidc_request->{$param} = $req->param($param); + $self->lmLog( + "OIDC request parameter $param: " + . $oidc_request->{$param}, + 'debug' + ); + } } # Detect requested flow @@ -244,7 +247,7 @@ sub run { # Check if user needs to be reauthenticated my $reauthentication = 0; my $prompt = $oidc_request->{'prompt'}; - if ( $prompt =~ /\blogin\b/ ) { + if ( $prompt and $prompt =~ /\blogin\b/ ) { $self->lmLog( "Reauthentication requested by Relying Party in prompt parameter", 'debug' @@ -439,10 +442,12 @@ sub run { $ask_for_consent = 1 if ( $prompt =~ /\bconsent\b/ ); } if ($ask_for_consent) { - if ( $self->param('confirm') == 1 ) { - $self->updatePersistentSession( + if ( $req->param('confirm') + and $req->param('confirm') == 1 ) + { + $self->p->updatePersistentSession( { "_oidc_consent_time_$rp" => time } ); - $self->updatePersistentSession( + $self->p->updatePersistentSession( { "_oidc_consent_scope_$rp" => $oidc_request->{'scope'} @@ -451,7 +456,9 @@ sub run { $self->lmLog( "Consent given for Relying Party $rp", 'debug' ); } - elsif ( $req->param('confirm') == -1 ) { + elsif ( $req->param('confirm') + and $req->param('confirm') == -1 ) + { $self->lmLog( "User refused consent for Relying party $rp", 'debug' ); @@ -471,7 +478,7 @@ sub run { 'debug' ); # Return error if prompt is none - if ( $prompt =~ /\bnone\b/ ) { + if ( $prompt and $prompt =~ /\bnone\b/ ) { $self->lmLog( "Consent is needed but prompt is none", 'debug' ); @@ -501,10 +508,10 @@ sub run { } # HERE - $self->info(''); $req->datas->{activeTimer} = 0; return PE_CONFIRM; } @@ -833,7 +840,7 @@ qq'

The application $display_name would li # Ask consent for logout if ( $req->param('confirm') ) { - if ( $self->param('confirm') == 1 ) { + if ( $req->param('confirm') == 1 ) { my $apacheSession = $self->p->getApacheSession( $req->id ); $self->p->_deleteSession($apacheSession); } @@ -853,7 +860,7 @@ qq'

The application $display_name would li return $req->param('confirm') == 1 ? PE_LOGOUT_OK : PE_OK; } - $self->info( + $req->info( '

Do you want to logout?

' ); $req->datas->{activeTimer} = 0; @@ -867,6 +874,7 @@ qq'

The application $display_name would li # Handle token endpoint sub token { my ( $self, $req ) = @_; + $req->parseBody if($req->method =~ /^post$/i); $self->lmLog( "URL detected as an OpenID Connect TOKEN URL", 'debug' ); # Check authentication @@ -903,7 +911,7 @@ sub token { } # Get code session - my $code = $self->param('code'); + my $code = $req->param('code'); $self->lmLog( "OpenID Connect Code: $code", 'debug' ); @@ -1025,6 +1033,7 @@ sub token { sub userInfo { my ( $self, $req ) = @_; $self->lmLog( "URL detected as an OpenID Connect USERINFO URL", 'debug' ); + $req->parseBody if($req->method =~ /^post$/i); my $access_token = $self->getEndPointAccessToken($req); @@ -1081,6 +1090,7 @@ sub userInfo { sub jwks { my ( $self, $req ) = @_; $self->lmLog( "URL detected as an OpenID Connect JWKS URL", 'debug' ); + $req->parseBody if($req->method =~ /^post$/i); my $jwks = { keys => [] }; @@ -1210,10 +1220,11 @@ sub endSessionDone { my ( $self, $req ) = @_; $self->lmLog( "URL detected as an OpenID Connect END SESSION URL", 'debug' ); + $req->parseBody if($req->method =~ /^post$/i); $self->lmLog( "User is already logged out", 'debug' ); - my $post_logout_redirect_uri = $self->param('post_logout_redirect_uri'); - my $state = $self->param('state'); + my $post_logout_redirect_uri = $req->param('post_logout_redirect_uri'); + my $state = $req->param('state'); if ($post_logout_redirect_uri) { @@ -1234,6 +1245,7 @@ sub checkSession { my ( $self, $req ) = @_; $self->lmLog( "URL detected as an OpenID Connect CHECK SESSION URL", 'debug' ); + $req->parseBody if($req->method =~ /^post$/i); my $portalPath = $self->{portal}; $portalPath =~ s#^https?://[^/]+/?#/#; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm index 02455c336..cd19b5141 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm @@ -69,7 +69,7 @@ sub loadOPs { } # Extract JSON data - foreach ( keys %{ $self->{oidcOPMetaDataJSON} } ) { + foreach ( keys %{ $self->conf->{oidcOPMetaDataJSON} } ) { $self->oidcOPList->{$_}->{conf} = $self->decodeJSON( $self->conf->{oidcOPMetaDataJSON}->{$_} ); $self->oidcOPList->{$_}->{jwks} = @@ -209,7 +209,7 @@ sub getCallbackUri { my $callback_uri = $self->conf->{portal}; $callback_uri .= - ( $self->{portal} =~ /\?/ ) + ( $self->conf->{portal} =~ /\?/ ) ? '&' . $callback_get_param . '=1' : '?' . $callback_get_param . '=1'; @@ -693,12 +693,13 @@ sub getOpenIDConnectSession { # corresponding session_id # @return State Session ID sub storeState { - my ( $self, @data ) = @_; + my ( $self, $req, @data ) = @_; # check if there are data to store my $infos; foreach (@data) { - $infos->{$_} = $self->{$_} if $self->{$_}; + $infos->{$_} = $req->{$_} if $req->{$_}; + $infos->{"datas_$_"} = $req->datas->{$_} if $req->datas->{$_}; } return unless ($infos); @@ -738,12 +739,16 @@ sub extractState { # Push values in $self foreach ( keys %{ $stateSession->data } ) { - next if $_ =~ /(type|_session_id|_utime)/; - if ( $req->can($_) ) { - $req->$_( $stateSession->data->{$_} ); + next if $_ =~ /(type|_session_id|_session_kind|_utime)/; + my $tmp = $stateSession->data->{$_}; + if (s/^datas_//) { + $req->datas->{$_} = $tmp; + } + elsif ( $req->can($_) ) { + $req->$_($tmp); } else { - $req->datas->{$_} = $stateSession->data->{$_}; + $self->lmLog( "Unknown request property $_, skipping", 'warn' ); } } @@ -1071,7 +1076,7 @@ sub getEndPointAuthenticationCredentials { my ( $client_id, $client_secret ); my $authorization = $req->authorization; - if ( $authorization =~ /^Basic (\w+)/i ) { + if ( $authorization and $authorization =~ /^Basic (\w+)/i ) { $self->lmLog( "Method client_secret_basic used", 'debug' ); eval { ( $client_id, $client_secret ) = split( /:/, decode_base64($1) ); diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm index 26a25ffd8..7e263fd07 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm @@ -59,53 +59,11 @@ sub display { } - # 1. Good authentication + # 1. Authentication not complete - # 1.1 Case : there is a message to display - if ( my $info = $req->info() ) { - $skinfile = 'info'; - %templateParams = ( - AUTH_ERROR_TYPE => $req->error_type, - MSG => $info, - URL => $req->{urldc}, - HIDDEN_INPUTS => $self->buildHiddenForm(), - ACTIVE_TIMER => $req->datas->{activeTimer}, - FORM_METHOD => $self->conf->{infoFormMethod}, - ); - } - - # 1.2 Redirection - elsif ( $req->{error} == PE_REDIRECT ) { - $skinfile = "redirect"; - %templateParams = ( - URL => $req->{urldc}, - HIDDEN_INPUTS => $self->buildHiddenForm($req), - FORM_METHOD => $req->datas->{redirectFormMethod} || 'get', - ); - } - - # 1.3 Case : display menu - elsif ( $req->error == PE_OK ) { - - $skinfile = 'menu'; - - #utf8::decode($auth_user); - - %templateParams = ( - AUTH_USER => $req->{sessionInfo}->{ $self->conf->{portalUserAttr} }, - NEWWINDOW => $self->conf->{portalOpenLinkInNewWindow}, - LOGOUT_URL => $self->conf->{portal} . "?logout=1", - APPSLIST_ORDER => $req->{sessionInfo}->{'appsListOrder'}, - PING => $self->conf->{portalPingInterval}, - $self->menu->params($req), - ); - } - - # 2. Authentication not complete - - # 2.1 A notification has to be done (session is created but hidden and + # 1.1 A notification has to be done (session is created but hidden and # unusable until the user has accept the message) - elsif ( my $notif = $req->datas->{notification} ) { + if ( my $notif = $req->datas->{notification} ) { $skinfile = 'notification'; %templateParams = ( AUTH_ERROR_TYPE => $req->error_type, @@ -117,7 +75,7 @@ sub display { ); } - # 2.2 An authentication (or userDB) module needs to ask a question + # 1.2 An authentication (or userDB) module needs to ask a question # before processing to the request elsif ( $req->{error} == PE_CONFIRM ) { $skinfile = 'confirm'; @@ -140,8 +98,8 @@ sub display { ); } - # 2.3 There is a message to disp->conf->conflay - elsif ( $info = $req->info ) { + # 1.3 There is a message to display + elsif ( my $info = $req->info ) { $skinfile = 'info'; %templateParams = ( AUTH_ERROR => $self->error, @@ -156,7 +114,7 @@ sub display { ); } - # 2.4 OpenID menu page + # 1.4 OpenID menu page elsif ($req->{error} == PE_OPENID_EMPTY or $req->{error} == PE_OPENID_BADID ) { @@ -175,7 +133,36 @@ sub display { ); } - # 2.5 Authentication has been refused OR this is the first access + # 2. Good authentication + + # 2.1 Redirection + elsif ( $req->{error} == PE_REDIRECT ) { + $skinfile = "redirect"; + %templateParams = ( + URL => $req->{urldc}, + HIDDEN_INPUTS => $self->buildHiddenForm($req), + FORM_METHOD => $req->datas->{redirectFormMethod} || 'get', + ); + } + + # 2.2 Case : display menu + elsif ( $req->error == PE_OK ) { + + $skinfile = 'menu'; + + #utf8::decode($auth_user); + + %templateParams = ( + AUTH_USER => $req->{sessionInfo}->{ $self->conf->{portalUserAttr} }, + NEWWINDOW => $self->conf->{portalOpenLinkInNewWindow}, + LOGOUT_URL => $self->conf->{portal} . "?logout=1", + APPSLIST_ORDER => $req->{sessionInfo}->{'appsListOrder'}, + PING => $self->conf->{portalPingInterval}, + $self->menu->params($req), + ); + } + + # 3 Authentication has been refused OR this is the first access else { $skinfile = 'login'; my $login = $self->userId($req); diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm index 0206c9e09..4ec462670 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm @@ -107,10 +107,10 @@ sub _forAuthUser { } sub _pForAuthUser { - my ( $self, $req ) = @_; + my ( $self, $req, @path ) = @_; $self->lmLog( 'Parsing posted datas', 'debug' ); $req->parseBody; - return $self->_forAuthUser($req); + return $self->_forAuthUser( $req, @path ); } 1; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm index e1a892f02..9fe8a92de 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm @@ -52,9 +52,28 @@ sub importHandlerDatas { PE_OK; } -# Verify url parameter +# Verify url and confirm parameter sub controlUrl { my ( $self, $req ) = @_; + if ( my $c = $req->param('confirm') ) { + + # Replace confirm stamp by 1 or -1 + $c =~ s/^(-?)(.*)$/${1}1/; + + # Decrypt confirm stamp if cipher available + # and confirm not already decrypted + if ( $self->conf->{cipher} and $2 ne "1" ) { + my $time = time() - $self->conf->{cipher}->decrypt($2); + if ( $time < 600 ) { + $self->lmLog( "Confirm parameter accepted $c", 'debug' ); + $req->param( 'confirm', $c ); + } + else { + $self->lmLog( 'Confirmation to old, refused', 'notice' ); + $req->param( 'confirm', 0 ); + } + } + } $req->{datas}->{_url} ||= ''; if ( my $url = $req->param('url') ) { diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm index d2449bb28..c15a3ffde 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm @@ -50,9 +50,6 @@ has checkLogins => ( is => 'rw' ); # Boolean to indicate that url isn't Base64 encoded has urlNotBase64 => ( is => 'rw' ); -# Info to display at login -has info => ( is => 'rw' ); - # Menu error has menuError => ( is => 'rw' ); diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm index 32830a565..527713493 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm @@ -15,6 +15,7 @@ extends 'Lemonldap::NG::Common::Module', 'Lemonldap::NG::Portal::Lib::OpenIDConn sub init { my ($self) = @_; + return $self->loadOPs; } # RUNNING METHODS diff --git a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC.t b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC.t index ffdd106ae..2ba4b9ad9 100644 --- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC.t +++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC.t @@ -7,7 +7,7 @@ BEGIN { require 't/test-lib.pm'; } -my $debug = 'error'; +my $debug = 'debug'; my ( $issuer, $sp, $res ); my %handlerOR = ( issuer => [], sp => [] ); @@ -25,11 +25,105 @@ switch ('sp'); ok( $sp = sp( $jwks, $metadata ), 'RP portal' ); count(1); -#print STDERR Dumper( $jwks, $metadata ); +# Query RP for auth +ok( $res = $sp->_get( '/', accept => 'text/html' ), 'Unauth SP request' ); +count(1); +my ( $url, $query ) = + expectRedirection( $res, qr#http://auth.op.com(/oauth2/authorize)\?(.*)$# ); + +# Push request to OP +switch ('issuer'); +ok( $res = $issuer->_get( $url, query => $query, accept => 'text/html' ), + 'Push request to OP' ); +count(1); +expectOK($res); + +# Try to authenticate to IdP +$query = "user=dwho&password=dwho&$query"; +ok( + $res = $issuer->_post( + $url, + IO::String->new($query), + accept => 'text/html', + length => length($query), + ), + 'Post authentication' +); +count(1); +my $idpId = expectCookie($res); +my ( $host, $tmp ); +( $host, $tmp, $query ) = expectForm( $res, '#' ); + +ok( + $res = $issuer->_post( + $url, + IO::String->new($query), + accept => 'text/html', + cookie => "lemonldap=$idpId", + length => length($query), + ), + 'Post authentication' +); +count(1); + +($query) = expectRedirection( $res, qr#^http://auth.rp.com/?\?(.*)$# ); + +# Push OP response to RP +switch ('sp'); + +ok( $res = $sp->_get( '/', query => $query, accept => 'text/html' ), + 'Call openidconnectcallback on RP' ); +count(1); +my $rpId = expectCookie($res); clean_sessions(); done_testing( count() ); +no warnings 'redefine'; + +sub LWP::UserAgent::request { + my ( $self, $req ) = @_; + ok( $req->uri =~ m#http://auth.((?:o|r)p).com(.*)#, 'REST request' ); + my $host = $1; + my $url = $2; + my $res; + my $client = ( $host eq 'op' ? $issuer : $sp ); + if ( $req->method =~ /^post$/i ) { + my $s = $req->content; + ok( + $res = $client->_post( + $url, IO::String->new($s), + length => length($s), + type => $req->header('Content-Type'), + ), + 'Execute request' + ); + } + else { + ok( + $res = $client->_get( + $url, + custom => { + HTTP_AUTHORIZATION => $req->header('Authorization'), + } + ), + 'Execute request' + ); + } + expectOK($res); + ok( getHeader( $res, 'Content-Type' ) =~ m#^application/json#, + 'Content is JSON' ) + or explain( $res->[1], 'Content-Type => application/json' ); + my $httpResp = HTTP::Response->new( $res->[0], 'OK' ); + + while ( my $name = shift @{ $res->[1] } ) { + $httpResp->header( $name, shift( @{ $res->[1] } ) ); + } + $httpResp->content( join( '', @{ $res->[2] } ) ); + count(3); + return $httpResp; +} + sub switch { my $type = shift; @Lemonldap::NG::Handler::Main::Reload::_onReload = @{ diff --git a/lemonldap-ng-portal/t/lmConf-1.js b/lemonldap-ng-portal/t/lmConf-1.js index 85ae008b0..c55259634 100644 --- a/lemonldap-ng-portal/t/lmConf-1.js +++ b/lemonldap-ng-portal/t/lmConf-1.js @@ -66,6 +66,12 @@ "LockDirectory": "t/sessions/saml/lock", "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" }, + "oidcStorage": "Apache::Session::File", + "oidcStorageOptions": { + "Directory": "t/sessions/saml", + "LockDirectory": "t/sessions/saml/lock", + "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" + }, "reloadUrls": {}, "userDB": "Demo", "whatToTrace": "_whatToTrace" diff --git a/lemonldap-ng-portal/t/test-lib.pm b/lemonldap-ng-portal/t/test-lib.pm index cdba6d8e8..aeef03c31 100644 --- a/lemonldap-ng-portal/t/test-lib.pm +++ b/lemonldap-ng-portal/t/test-lib.pm @@ -48,33 +48,46 @@ sub expectRedirection { my ( $res, $location ) = @_; ok( $res->[0] == 302, 'Get redirection' ) or explain( $res->[0], 302 ); - count(2); + count(1); if ( ref $location ) { my @match; @match = ( getRedirection($res) =~ $location ); ok( @match, 'Location header found' ) or explain( $res->[1], "Location match: " . Dumper($location) ); + count(1); return @match; } else { ok( getRedirection($res) eq $location, "Location is $location" ) or explain( $res->[1], "Location => $location" ); +count(1); } } sub expectAutoPost { + my @r = expectForm(@_); + my $method = pop @r; + ok ( $method =~ /^post$/i ); + count(1); +} + +sub expectForm { my ( $res, $hostRe, $uriRe, @requiredFields ) = @_; expectOK($res); - count(2); + count(1); if ( ok( $res->[2]->[0] =~ - m@[2]->[0] =~ - m#