## @file # OpenIDConnect Issuer file ## @class # OpenIDConnect Issuer class package Lemonldap::NG::Portal::IssuerDBOpenIDConnect; use strict; use Lemonldap::NG::Portal::Simple; use String::Random qw(random_string); use base qw(Lemonldap::NG::Portal::_OpenIDConnect); our $VERSION = '2.0.0'; ## @method void issuerDBInit() # Get configuration data # @return Lemonldap::NG::Portal error code sub issuerDBInit { my $self = shift; return PE_ERROR unless $self->loadRPs; return PE_OK; } ## @apmethod int issuerForUnAuthUser() # Get OIDC request # @return Lemonldap::NG::Portal error code sub issuerForUnAuthUser { my $self = shift; my $issuerDBOpenIDConnectPath = $self->{issuerDBOpenIDConnectPath}; my $authorize_uri = $self->{oidcServiceMetaDataAuthorizeURI}; my $token_uri = $self->{oidcServiceMetaDataTokenURI}; my $userinfo_uri = $self->{oidcServiceMetaDataUserInfoURI}; my $jwks_uri = $self->{oidcServiceMetaDataJWKSURI}; my $registration_uri = $self->{oidcServiceMetaDataRegistrationURI}; my $endsession_uri = $self->{oidcServiceMetaDataEndSessionURI}; my $checksession_uri = $self->{oidcServiceMetaDataCheckSessionURI}; my $issuer = $self->{oidcServiceMetaDataIssuer}; # Called URL my $url = $self->url(); my $url_path = $self->url( -absolute => 1 ); $url_path =~ s#^//#/#; # AUTHORIZE if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${authorize_uri}# ) { $self->lmLog( "URL $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_values request request_uri/ ) { $oidc_request->{$param} = $self->getHiddenFormValue($param) || $self->param($param); $self->lmLog( "OIDC request parameter $param: " . $oidc_request->{$param}, 'debug' ); $self->setHiddenFormValue( $param, $oidc_request->{$param} ); } # 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->{$_} ); } } # State my $state = $oidc_request->{'state'}; # Check redirect_uri my $redirect_uri = $oidc_request->{'redirect_uri'}; unless ($redirect_uri) { $self->lmLog( "Redirect URI is required", 'error' ); return PE_ERROR; } # Check display my $display = $oidc_request->{'display'}; if ( $display eq "page" ) { $self->lmLog( "Display type page will be used", 'debug' ); } else { $self->lmLog( "Display type $display not supported, display type page will be used", 'debug' ); } # Check prompt my $prompt = $oidc_request->{'prompt'}; if ( $prompt =~ /\bnone\b/ ) { $self->lmLog( "Prompt type none requested, but user needs to authenticate", 'error' ); $self->returnRedirectError( $redirect_uri, "login_required", "Prompt type none requested", undef, $state, ( $flow ne "authorizationcode" ) ); } # Check ui_locales my $ui_locales = $oidc_request->{'ui_locales'}; if ( defined $ui_locales ) { my $lang = join( ',', split( /\s+/, $ui_locales ) ); $self->{lang} = $self->extract_lang($lang); } # Check login_hint my $login_hint = $oidc_request->{'login_hint'}; if ( defined $login_hint ) { $self->{user} ||= $login_hint; } } # TOKEN if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${token_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect TOKEN URL", 'debug' ); # Check authentication my ( $client_id, $client_secret ) = $self->getEndPointAuthenticationCredentials(); unless ( $client_id && $client_secret ) { $self->lmLog( "No authentication provided to get token, or authentication type not supported", "error" ); $self->returnJSONError("unauthorized_client"); $self->quit; } # 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' ); $self->returnJSONError("unauthorized_client"); $self->quit; } else { $self->lmLog( "Client id $client_id match RP $rp", 'debug' ); } # Check client_secret unless ( $client_secret eq $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsClientSecret} ) { $self->lmLog( "Wrong credentials", "error" ); $self->returnJSONError("access_denied"); $self->quit; } # Get code session my $code = $self->param('code'); $self->lmLog( "OpenID Connect Code: $code", 'debug' ); my $codeSession = $self->getOpenIDConnectSession($code); unless ($codeSession) { $self->lmLog( "Unable to find OIDC session $code", "error" ); $self->returnJSONError("invalid_request"); $self->quit; } # Check we have the same redirect_uri value unless ( $self->param("redirect_uri") eq $codeSession->data->{redirect_uri} ) { $self->lmLog( "Provided redirect_uri is different from " . $codeSession->{redirect_uri}, "error" ); $self->returnJSONError("invalid_request"); $codeSession->remove(); $self->quit; } # Get user identifier my $apacheSession = $self->getApacheSession( $codeSession->data->{user_session_id}, 1 ); unless ($apacheSession) { $self->lmLog( "Unable to find user session linked to OIDC session $code", "error" ); $self->returnJSONError("invalid_request"); $codeSession->remove(); $self->quit; } my $user_id_attribute = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsUserIDAttr} || $self->{whatToTrace}; my $user_id = $apacheSession->data->{$user_id_attribute}; $self->lmLog( "Found corresponding user: $user_id", 'debug' ); # Generate access_token my $accessTokenSession = $self->getOpenIDConnectSession; unless ($accessTokenSession) { $self->lmLog( "Unable to create OIDC session for access_token", "error" ); $self->returnJSONError("invalid_request"); $codeSession->remove(); $self->quit; } # Store data in access token $accessTokenSession->update( { scope => $codeSession->data->{scope}, rp => $rp, user_session_id => $apacheSession->id, _utime => time, } ); my $access_token = $accessTokenSession->id; $self->lmLog( "Generated access token: $access_token", 'debug' ); # Compute hash to store in at_hash my $alg = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenSignAlg}; my ($hash_level) = ( $alg =~ /(?:\w{2})(\d{3})/ ); my $at_hash = $self->createHash( $access_token, $hash_level ); # ID token payload my $id_token_exp = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenExpiration}; $id_token_exp += time; my $id_token_acr = "loa-" . $apacheSession->data->{authenticationLevel}; my $id_token_payload_hash = { iss => $issuer, # Issuer Identifier sub => $user_id, # Subject Identifier aud => [$client_id], # Audience exp => $id_token_exp, # expiration iat => time, # Issued time auth_time => $apacheSession->data->{_lastAuthnUTime}, # Authentication time acr => $id_token_acr, # Authentication Context Class Reference azp => $client_id, # Authorized party # TODO amr }; my $nonce = $codeSession->data->{nonce}; $id_token_payload_hash->{nonce} = $nonce if defined $nonce; $id_token_payload_hash->{'at_hash'} = $at_hash if $at_hash; # Create ID Token my $id_token = $self->createIDToken( $id_token_payload_hash, $rp ); $self->lmLog( "Generated id token: $id_token", 'debug' ); # Send token response my $expires_in = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsAccessTokenExpiration}; my $token_response = { access_token => $access_token, token_type => 'Bearer', expires_in => $expires_in, id_token => $id_token, }; $self->returnJSON($token_response); $self->lmLog( "Token response sent", 'debug' ); $codeSession->remove(); $self->quit; } # USERINFO if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${userinfo_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect USERINFO URL", 'debug' ); my $access_token = $self->getEndPointAccessToken(); unless ($access_token) { $self->lmLog( "Unable to get access_token", "error" ); $self->returnBearerError( "invalid_request", "Access token not found in request" ); $self->quit; } $self->lmLog( "Received Access Token $access_token", 'debug' ); my $accessTokenSession = $self->getOpenIDConnectSession($access_token); unless ($accessTokenSession) { $self->lmLog( "Unable to get access token session for id $access_token", "error" ); $self->returnBearerError( "invalid_token", "Access Token not found or expired" ); $self->quit; } # Get access token session data my $scope = $accessTokenSession->data->{scope}; my $rp = $accessTokenSession->data->{rp}; my $user_session_id = $accessTokenSession->data->{user_session_id}; my $userinfo_response = $self->buildUserInfoResponse( $scope, $rp, $user_session_id ); my $userinfo_sign_alg = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsUserInfoSignAlg}; unless ($userinfo_sign_alg) { $self->returnJSON($userinfo_response); } else { my $userinfo_jwt = $self->createJWT( $userinfo_response, $userinfo_sign_alg, $rp ); print $self->header('application/jwt'); print $userinfo_jwt; $self->lmLog( "Return UserInfo as JWT: $userinfo_jwt", 'debug' ); } $self->lmLog( "UserInfo response sent", 'debug' ); $self->quit; } # JWKS if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${jwks_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect JWKS URL", 'debug' ); my $jwks = { keys => [] }; my $public_key_sig = $self->{oidcServicePublicKeySig}; my $key_id_sig = $self->{oidcServiceKeyIdSig}; if ($public_key_sig) { my $key = $self->key2jwks($public_key_sig); $key->{kty} = "RSA"; $key->{use} = "sig"; $key->{kid} = $key_id_sig if $key_id_sig; push @{ $jwks->{keys} }, $key; } $self->returnJSON($jwks); $self->lmLog( "JWKS response sent", 'debug' ); $self->quit; } # REGISTRATION if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${registration_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect REGISTRATION URL", 'debug' ); # TODO: check Initial Access Token # Specific message to allow DOS detection my $source_ip = $self->ipAddr; $self->lmLog( "OpenID Connect Registration request from $source_ip", 'warn' ); # Check dynamic registration is allowed unless ( $self->{oidcServiceAllowDynamicRegistration} ) { $self->lmLog( "Dynamic registration is not allowed", 'error' ); $self->returnJSONError( 'server_error', 'Dynamic registration is not allowed' ); $self->quit; } # Get client metadata my $client_metadata_json = $self->param('POSTDATA'); $self->lmLog( "Client metadata received: $client_metadata_json", 'debug' ); my $client_metadata = $self->decodeJSON($client_metadata_json); my $registration_response = {}; # Check redirect_uris unless ( $client_metadata->{redirect_uris} ) { $self->lmLog( "Field redirect_uris is mandatory", 'error' ); $self->returnJSONError( 'invalid_client_metadata', 'Field redirect_uris is mandatory' ); $self->quit; } # RP identifier my $registration_time = time; my $rp = "register-$registration_time"; # Generate Client ID and Client Password my $client_id = random_string("ssssssssssssssssssssssssssssss"); my $client_secret = random_string("ssssssssssssssssssssssssssssss"); # Register known parameters my $client_name = $client_metadata->{client_name} || "Self registered client"; my $logo_uri = $client_metadata->{logo_uri}; my $id_token_signed_response_alg = $client_metadata->{id_token_signed_response_alg} || "RS256"; my $userinfo_signed_response_alg = $client_metadata->{userinfo_signed_response_alg}; my $redirect_uris = $client_metadata->{redirect_uris}; # Register RP in global configuration my $conf = $self->__lmConf->getConf(); $conf->{cfgAuthor} = "OpenID Connect Registration ($client_name)"; $conf->{cfgAuthorIP} = $source_ip; $conf->{oidcRPMetaDataExportedVars}->{$rp} = {}; $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsClientID} = $client_id; $conf->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsClientSecret} = $client_secret; $conf->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsDisplayName} = $client_name; $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsIcon} = $logo_uri; $conf->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenSignAlg} = $id_token_signed_response_alg; $conf->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsRedirectUris} = join( ' ', @$redirect_uris ); $conf->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsUserInfoSignAlg} = $userinfo_signed_response_alg if defined $userinfo_signed_response_alg; if ( $self->__lmConf->saveConf($conf) ) { # Reload RP list $self->loadRPs(1); # Send registration response $registration_response->{'client_id'} = $client_id; $registration_response->{'client_secret'} = $client_secret; $registration_response->{'client_id_issued_at'} = $registration_time; $registration_response->{'client_id_expires_at'} = 0; $registration_response->{'client_name'} = $client_name; $registration_response->{'logo_uri'} = $logo_uri; $registration_response->{'id_token_signed_response_alg'} = $id_token_signed_response_alg; $registration_response->{'redirect_uris'} = $redirect_uris; $registration_response->{'userinfo_signed_response_alg'} = $userinfo_signed_response_alg if defined $userinfo_signed_response_alg; } else { $self->lmLog( "Configuration not saved: $Lemonldap::NG::Common::Conf::msg", 'error' ); $self->returnJSONError( 'server_error', 'Configuration not saved' ); $self->quit; } # TODO: return 201 HTTP code $self->returnJSON($registration_response); $self->lmLog( "Registration response sent", 'debug' ); $self->quit; } # END SESSION if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${endsession_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect END SESSION URL", 'debug' ); $self->lmLog( "User is already logged out", 'debug' ); my $post_logout_redirect_uri = $self->param('post_logout_redirect_uri'); my $state = $self->param('state'); if ($post_logout_redirect_uri) { # Build Response my $response_url = $self->buildLogoutResponse( $post_logout_redirect_uri, $state ); $self->lmLog( "Redirect user to $response_url", 'debug' ); $self->{'urldc'} = $response_url; $self->_sub('autoRedirect'); } return PE_LOGOUT_OK; } # CHECK SESSION if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${checksession_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect CHECK SESSION URL", 'debug' ); print $self->header( -type => 'text/html', -access_control_allow_origin => '*' ); print $self->start_html( -title => 'Check Session', -script => [ { -type => 'text/javascript', -src => 'http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha256.js' }, { -type => 'text/javascript', -src => 'http://crypto-js.googlecode.com/svn/tags/3.1.2/build/components/enc-base64-min.js' }, { -code => $self->getSessionManagementOPIFrameJS } ] ); print $self->end_html(); $self->quit(); } PE_OK; } ## @apmethod int issuerForAuthUser() # Do nothing # @return Lemonldap::NG::Portal error code sub issuerForAuthUser { my $self = shift; my $issuerDBOpenIDConnectPath = $self->{issuerDBOpenIDConnectPath}; my $authorize_uri = $self->{oidcServiceMetaDataAuthorizeURI}; my $token_uri = $self->{oidcServiceMetaDataTokenURI}; my $userinfo_uri = $self->{oidcServiceMetaDataUserInfoURI}; my $jwks_uri = $self->{oidcServiceMetaDataJWKSURI}; my $registration_uri = $self->{oidcServiceMetaDataRegistrationURI}; my $endsession_uri = $self->{oidcServiceMetaDataEndSessionURI}; my $checksession_uri = $self->{oidcServiceMetaDataCheckSessionURI}; my $issuer = $self->{oidcServiceMetaDataIssuer}; # Session ID my $session_id = $self->{sessionInfo}->{_session_id} || $self->{id}; # Called URL my $url = $self->url(); my $url_path = $self->url( -absolute => 1 ); $url_path =~ s#^//#/#; # AUTHORIZE if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${authorize_uri}# ) { $self->lmLog( "URL $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} = $self->getHiddenFormValue($param) || $self->param($param); $self->lmLog( "OIDC request parameter $param: " . $oidc_request->{$param}, 'debug' ); $self->setHiddenFormValue( $param, $oidc_request->{$param} ); } # 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( $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' ); $self->returnRedirectError( $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' ); $self->returnRedirectError( $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->{oidcServiceAllowAuthorizationCodeFlow} ) { $self->lmLog( "Authorization code flow is not allowed", 'error' ); $self->returnRedirectError( $oidc_request->{'redirect_uri'}, "server_error", "Authorization code flow not allowed", undef, $oidc_request->{'state'}, 0 ); } if ( $flow eq "implicit" and not $self->{oidcServiceAllowImplicitFlow} ) { $self->lmLog( "Implicit flow is not allowed", 'error' ); $self->returnRedirectError( $oidc_request->{'redirect_uri'}, "server_error", "Implicit flow not allowed", undef, $oidc_request->{'state'}, 1 ); } if ( $flow eq "hybrid" and not $self->{oidcServiceAllowHybridFlow} ) { $self->lmLog( "Hybrid flow is not allowed", 'error' ); $self->returnRedirectError( $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 = $self->{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) { # Replay authentication process $self->{updateSession} = 1; $self->{error} = $self->_subProcess( qw(issuerDBInit authInit issuerForUnAuthUser extractFormInfo userDBInit getUser setAuthSessionInfo setSessionInfo setMacros setGroups setPersistentSessionInfo setLocalGroups authenticate store authFinish) ); # Return error if any return $self->{error} if ( $self->{error} > 0 ); # Disable further reauthentication $prompt =~ s/\blogin\b//; $self->setHiddenFormValue( 'prompt', $prompt ); # Update session_id $session_id = $self->{sessionInfo}->{_session_id} || $self->{id}; } # 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' ); $self->returnRedirectError( $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->{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' ); $self->returnRedirectError( $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->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsUserIDAttr} || $self->{whatToTrace}; my $user_id = $self->{sessionInfo}->{$user_id_attribute}; unless ( $sub eq $user_id ) { $self->lmLog( "ID Token hint sub $sub do not match user $user_id", 'error' ); $self->returnRedirectError( $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 $ask_for_consent = 1; if ( $self->{sessionInfo}->{"_oidc_consent_time_$rp"} and $self->{sessionInfo}->{"_oidc_consent_scope_$rp"} ) { $ask_for_consent = 0; my $consent_time = $self->{sessionInfo}->{"_oidc_consent_time_$rp"}; my $consent_scope = $self->{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 ( $self->param('confirm') == -1 ) { $self->lmLog( "User refused consent for Relying party $rp", 'debug' ); $self->returnRedirectError( $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' ); $self->returnRedirectError( $oidc_request->{'redirect_uri'}, "consent_required", "consent required", undef, $oidc_request->{'state'}, ( $flow ne "authorizationcode" ) ); } my $display_name = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsDisplayName}; my $icon = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIcon}; my $img_src; my $portalPath = $self->{portal}; $portalPath =~ s#^https?://[^/]+/?#/#; $portalPath =~ s#[^/]+\.pl$##; if ($icon) { $img_src = ( $icon =~ m#^https?://# ) ? $icon : $portalPath . "skins/common/" . $icon; } $self->info(''); $self->{activeTimer} = 0; return PE_CONFIRM; } } # Create session_state my $session_state = $self->createSessionState( $session_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 => $session_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' ); $self->{'urldc'} = $response_url; $self->_sub('autoRedirect'); } # 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( $oidc_request->{'redirect_uri'}, "server_error", undef, undef, $oidc_request->{'state'}, 1 ); } # Store data in access token $accessTokenSession->update( { scope => $oidc_request->{'scope'}, rp => $rp, user_session_id => $session_id, _utime => time, } ); $access_token = $accessTokenSession->id; $self->lmLog( "Generated access token: $access_token", 'debug' ); # Compute hash to store in at_hash my $alg = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenSignAlg}; my ($hash_level) = ( $alg =~ /(?:\w{2})(\d{3})/ ); $at_hash = $self->createHash( $access_token, $hash_level ); } # ID token payload my $id_token_exp = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenExpiration}; $id_token_exp += time; my $authenticationLevel = $self->{sessionInfo}->{authenticationLevel}; my $id_token_acr; foreach ( keys %{ $self->{oidcServiceMetaDataAuthnContext} } ) { if ( $self->{oidcServiceMetaDataAuthnContext}->{$_} eq $authenticationLevel ) { $id_token_acr = $_; last; } } my $user_id_attribute = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsUserIDAttr} || $self->{whatToTrace}; my $user_id = $self->{sessionInfo}->{$user_id_attribute}; my $id_token_payload_hash = { iss => $issuer, # Issuer Identifier sub => $user_id, # Subject Identifier aud => [$client_id], # Audience exp => $id_token_exp, # expiration iat => time, # Issued time auth_time => $self->{sessionInfo}->{_lastAuthnUTime} , # Authentication time azp => $client_id, # Authorized party # TODO amr nonce => $oidc_request->{'nonce'} # Nonce }; $id_token_payload_hash->{'at_hash'} = $at_hash if $at_hash; $id_token_payload_hash->{'acr'} = $id_token_acr if $id_token_acr; # Create ID Token my $id_token = $self->createIDToken( $id_token_payload_hash, $rp ); $self->lmLog( "Generated id token: $id_token", 'debug' ); # Send token response my $expires_in = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsAccessTokenExpiration}; # Build Response my $response_url = $self->buildImplicitAuthnResponse( $oidc_request->{'redirect_uri'}, $access_token, $id_token, $expires_in, $oidc_request->{'state'}, $session_state ); $self->lmLog( "Redirect user to $response_url", 'debug' ); $self->{'urldc'} = $response_url; $self->_sub('autoRedirect'); } # Hybrid Flow if ( $flow eq "hybrid" ) { my $access_token; my $id_token; my $at_hash; my $c_hash; # Hash level my $alg = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenSignAlg}; my ($hash_level) = ( $alg =~ /(?:\w{2})(\d{3})/ ); # 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 => $session_id, _utime => time, nonce => $oidc_request->{'nonce'}, } ); # Compute hash to store in c_hash $c_hash = $self->createHash( $code, $hash_level ); 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( $oidc_request->{'redirect_uri'}, "server_error", undef, undef, $oidc_request->{'state'}, 1 ); } # Store data in access token $accessTokenSession->update( { scope => $oidc_request->{'scope'}, rp => $rp, user_session_id => $session_id, _utime => time, } ); $access_token = $accessTokenSession->id; $self->lmLog( "Generated access token: $access_token", 'debug' ); # Compute hash to store in at_hash $at_hash = $self->createHash( $access_token, $hash_level ); } if ( $response_type =~ /\bid_token\b/ ) { # ID token payload my $id_token_exp = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenExpiration}; $id_token_exp += time; my $id_token_acr = "loa-" . $self->{sessionInfo}->{authenticationLevel}; my $user_id_attribute = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsUserIDAttr} || $self->{whatToTrace}; my $user_id = $self->{sessionInfo}->{$user_id_attribute}; my $id_token_payload_hash = { iss => $issuer, # Issuer Identifier sub => $user_id, # Subject Identifier aud => [$client_id], # Audience exp => $id_token_exp, # expiration iat => time, # Issued time auth_time => $self->{sessionInfo}->{_lastAuthnUTime} , # Authentication time acr => $id_token_acr, # Authentication Context Class Reference azp => $client_id, # Authorized party # TODO amr nonce => $oidc_request->{'nonce'} # Nonce }; $id_token_payload_hash->{'at_hash'} = $at_hash if $at_hash; $id_token_payload_hash->{'c_hash'} = $c_hash if $c_hash; # Create ID Token $id_token = $self->createIDToken( $id_token_payload_hash, $rp ); $self->lmLog( "Generated id token: $id_token", 'debug' ); } my $expires_in = $self->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsAccessTokenExpiration}; # Build Response my $response_url = $self->buildHybridAuthnResponse( $oidc_request->{'redirect_uri'}, $code, $access_token, $id_token, $expires_in, $oidc_request->{'state'}, $session_state ); $self->lmLog( "Redirect user to $response_url", 'debug' ); $self->{'urldc'} = $response_url; $self->_sub('autoRedirect'); } $self->lmLog( "No flow has been selected", 'debug' ); return PE_OK; } # TOKEN if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${token_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect TOKEN URL", 'debug' ); # This should not happen $self->lmLog( "Token request found on an active SSO session, ignoring it", 'error' ); $self->returnJSONError("invalid_request"); $self->quit; } # USERINFO if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${userinfo_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect USERINFO URL", 'debug' ); # This should not happen $self->lmLog( "UserInfo request found on an active SSO session, ignoring it", 'error' ); $self->returnJSONError("invalid_request"); $self->quit; } # JWKS if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${jwks_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect JWKS URL", 'debug' ); # This should not happen $self->lmLog( "JWKS request found on an active SSO session, ignoring it", 'error' ); $self->returnJSONError("invalid_request"); $self->quit; } # REGISTRATION if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${registration_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect REGISTRATION URL", 'debug' ); # This should not happen $self->lmLog( "Registration request found on an active SSO session, ignoring it", 'error' ); $self->returnJSONError("invalid_request"); $self->quit; } # END SESSION if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${endsession_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect END SESSION URL", 'debug' ); # Set hidden fields my $oidc_request = {}; foreach my $param (qw/id_token_hint post_logout_redirect_uri state/) { $oidc_request->{$param} = $self->getHiddenFormValue($param) || $self->param($param); $self->lmLog( "OIDC request parameter $param: " . $oidc_request->{$param}, 'debug' ); $self->setHiddenFormValue( $param, $oidc_request->{$param} ); } my $post_logout_redirect_uri = $oidc_request->{'post_logout_redirect_uri'}; my $state = $oidc_request->{'state'}; # Ask consent for logout if ( $self->param('confirm') == 1 or $self->param('confirm') == -1 ) { if ( $self->param('confirm') == 1 ) { my $apacheSession = $self->getApacheSession($session_id); $self->_deleteSession($apacheSession); } if ($post_logout_redirect_uri) { # Build Response my $response_url = $self->buildLogoutResponse( $post_logout_redirect_uri, $state ); $self->lmLog( "Redirect user to $response_url", 'debug' ); $self->{'urldc'} = $response_url; $self->_sub('autoRedirect'); } return PE_LOGOUT_OK if $self->param('confirm') == 1; return PE_OK; } $self->info('
'); $self->info( '

' . $self->msg(PM_OIDC_CONFIRM_LOGOUT) . '

' ); $self->info('
'); $self->{activeTimer} = 0; return PE_CONFIRM; } # CHECK SESSION if ( $url_path =~ m#${issuerDBOpenIDConnectPath}${checksession_uri}# ) { $self->lmLog( "URL $url detected as an OpenID Connect CHECK SESSION URL", 'debug' ); print $self->header( -type => 'text/html', -access_control_allow_origin => '*' ); print $self->start_html( -title => 'Check Session', -script => [ { -type => 'text/javascript', -src => 'http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha256.js' }, { -type => 'text/javascript', -src => 'http://crypto-js.googlecode.com/svn/tags/3.1.2/build/components/enc-base64-min.js' }, { -code => $self->getSessionManagementOPIFrameJS } ] ); print $self->end_html(); $self->quit(); } PE_OK; } ## @apmethod int issuerLogout() # Do nothing # @return Lemonldap::NG::Portal error code sub issuerLogout { PE_OK; } 1; __END__ =head1 NAME =encoding utf8 Lemonldap::NG::Portal::IssuerDBOpenIDConnect - OpenIDConnect Provider for Lemonldap::NG =head1 DESCRIPTION This is an OpenID Connect provider implementation in LemonLDAP::NG =head1 SEE ALSO L =head1 AUTHOR =over =item Clement Oudot, Eclem.oudot@gmail.comE =back =head1 BUG REPORT Use OW2 system to report bug or ask for features: L =head1 DOWNLOAD Lemonldap::NG is available at L =head1 COPYRIGHT AND LICENSE =over =item Copyright (C) 2014-2016 by Clement Oudot, Eclem.oudot@gmail.comE =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. =cut