#!/usr/bin/perl -w # Simple OpenID Connect client use strict; use JSON; use LWP::UserAgent; use MIME::Base64 qw/encode_base64url encode_base64 decode_base64url decode_base64/; use URI::Escape; use CGI; use Data::Dumper; $Data::Dumper::Useperl = 1; use utf8; use Digest::SHA qw/hmac_sha256_base64 hmac_sha384_base64 hmac_sha512_base64 sha256 sha256_base64 sha384_base64 sha512_base64/; #============================================================================== # Configuration #============================================================================== my $host = $ENV{HTTP_HOST}; my ( $domain, $port ) = ( $host =~ /\w+\.([^:]+)(:\d+)?/ ); my $protocol = ( $ENV{HTTPS} =~ /^on$/i ) ? "https" : "http"; $port ||= ""; my $portal_url = "$protocol://auth.$domain$port"; my $client_id = "lemonldap"; my $client_secret = "secret"; my $authorization_uri = "$portal_url/oauth2/authorize"; my $token_uri = "$portal_url/oauth2/token"; my $userinfo_uri = "$portal_url/oauth2/userinfo"; my $registration_uri = "$portal_url/oauth2/register"; my $endsession_uri = "$portal_url/oauth2/logout"; my $checksession_uri = "$portal_url/oauth2/checksession"; #============================================================================== # CSS #============================================================================== my $css = <url . "?openidconnectcallback=1"; my $local_configuration_uri = $cgi->url . "?test=configuration"; my $local_registration_uri = $cgi->url . "?test=registration"; my $local_request_uri = $cgi->url . "?test=request"; my $local_checksession_uri = $cgi->url . "?test=checksession"; $Data::Dumper::Terse = 1; $Data::Dumper::Sortkeys = 1; # Request URI if ( $cgi->param("test") eq "request" ) { my $request_paylod_hash = { response_type => "code", scope => "openid profile", client_id => $client_id, redirect_uri => $redirect_uri, display => "page", iss => $client_id, aud => [$portal_url] }; my $request_payload = encode_base64( to_json($request_paylod_hash), "" ); my $request_header_hash = { typ => "JWT", alg => "HS256" }; my $request_header = encode_base64( to_json($request_header_hash), "" ); my $request_digest = hmac_sha256_base64( $request_header . "." . $request_payload, $client_secret ); $request_digest =~ s/\+/-/g; $request_digest =~ s/\//_/g; my $request = $request_header . "." . $request_payload . "." . $request_digest; print $cgi->header( -type => 'application/jwt;charset=utf-8' ); print $request; exit; } # Check Session if ( $cgi->param("test") eq "checksession" ) { my $session_state = $cgi->param("session_state"); my $js; $js .= 'var stat = "unchanged";' . "\n"; $js .= 'var mes = "' . uri_escape($client_id) . '" + " " + "' . uri_escape($session_state) . '";' . "\n"; $js .= 'function check_session()' . "\n"; $js .= '{' . "\n"; $js .= 'var targetOrigin = "' . $portal_url . '";' . "\n"; $js .= 'var win = window.parent.document.getElementById("opchecksession").contentWindow;' . "\n"; $js .= 'win.postMessage( mes, targetOrigin);' . "\n"; $js .= '}' . "\n"; $js .= 'function setTimer()' . "\n"; $js .= '{' . "\n"; $js .= 'check_session();' . "\n"; $js .= 'timerID = setInterval("check_session()",3*1000);' . "\n"; $js .= '}' . "\n"; $js .= 'window.addEventListener("message", receiveMessage, false);' . "\n"; $js .= 'function receiveMessage(e)' . "\n"; $js .= '{' . "\n"; $js .= 'var targetOrigin = "' . $portal_url . '";' . "\n"; $js .= 'if (e.origin !== targetOrigin ) {return;}' . "\n"; $js .= 'stat = e.data;' . "\n"; $js .= 'document.getElementById("sessionstatus").textContent=stat;' . "\n"; $js .= '}' . "\n"; $js .= 'setTimer();' . "\n"; print $cgi->header( -type => 'text/html' ); print $cgi->start_html( -title => 'Check Session', -script => $js ); print "

Session status:

\n"; print $cgi->end_html(); exit; } # Start HTTP response print $cgi->header( -type => 'text/html;charset=utf-8' ); print "\n"; print "\n"; print "\n"; print "OpenID Connect sample client\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "
\n"; print "
\n"; print "
\n"; print "

OpenID Connect sample client

\n"; print "
\n"; print "
\n"; # OIDC Callback my $callback = $cgi->param("openidconnectcallback"); if ($callback) { print "

Callback received

"; print "
\n"; print "
\n"; print "

OpenID Connect callback received

\n"; print "
\n"; print "
\n"; print "
"
      . $cgi->url( -path_info => 1, -query => 1 )
      . "
\n"; print "
\n"; print "
\n"; # Check Flow my $code = $cgi->param("code"); my $implicitcallback = $cgi->param("implicitcallback"); my $error = $cgi->param("error"); my $error_description = $cgi->param("error_description"); if ($error) { print "
"; print "

Error: $error

"; print "

Description: $error_description

" if $error_description; print "
"; print "\n"; print "
"; print "
"; print $cgi->end_html(); exit 0; } unless ( $code or $implicitcallback ) { print ' '; } # AuthN Response my $state = $cgi->param("state"); # Check state unless ( $state eq "ABCDEFGHIJKLMNOPQRSTUVWXXZ" ) { print "
"; print "

OpenIDConnect callback state $state is invalid

"; print "
"; print "\n"; print "
"; print ""; print $cgi->end_html(); exit 0; } my $access_token; my $id_token; if ($code) { my $grant_type = "authorization_code"; my %form; $form{"code"} = $code; $form{"redirect_uri"} = $redirect_uri; $form{"grant_type"} = $grant_type; # Method client_secret_post #$form{"client_id"} = $client_id; #$form{"client_secret"} = $client_secret; #my $response = $ua->post( $token_uri, \%form, # "Content-Type" => 'application/x-www-form-urlencoded' ); # Method client_secret_basic my $response = $ua->post( $token_uri, \%form, "Authorization" => "Basic " . encode_base64("$client_id:$client_secret"), "Content-Type" => 'application/x-www-form-urlencoded', ); if ( $response->is_error ) { print "
"; print "

Bad authorization response: " . $response->message . "

"; print "

$response->content

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } # Get access_token and id_token my $content = $response->decoded_content; my $json; eval { $json = from_json( $content, { allow_nonref => 1 } ); }; if ($@) { print "
"; print "

Wrong JSON content

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } if ( $json->{error} ) { print "
"; print "

Error in token response:" . $json->{error} . "

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } $access_token = $json->{access_token}; $id_token = $json->{id_token}; } else { $access_token = $cgi->param("access_token"); $id_token = $cgi->param("id_token"); } print "
\n"; print "
\n"; print "

Tokens

\n"; print "
\n"; print "
\n"; print "
Access token: $access_token
"; print "
ID token: $id_token
"; print "
\n"; print "
\n"; # Get ID token content my ( $id_token_header, $id_token_payload, $id_token_signature ) = split( /\./, $id_token ); # TODO check signature my $id_token_payload_hash = from_json( decode_base64($id_token_payload), { allow_nonref => 1 } ); print "
\n"; print "
\n"; print "

ID Token content

\n"; print "
\n"; print "
\n"; print "
" . Dumper($id_token_payload_hash) . "
"; print "
\n"; print "
\n"; # Request UserInfo my $ui_response = $ua->get( $userinfo_uri, "Authorization" => "Bearer $access_token" ); my $ui_content = $ui_response->decoded_content; my $ui_json; my $content_type = $ui_response->header('Content-Type'); if ( $content_type =~ /json/ ) { eval { $ui_json = from_json( $ui_content, { allow_nonref => 1 } ); }; } elsif ( $content_type =~ /jwt/ ) { my ( $ui_header, $ui_payload, $ui_signature ) = split( /\./, $ui_content ); eval { $ui_json = from_json( decode_base64($ui_payload), { allow_nonref => 1 } ); }; } print "
\n"; print "
\n"; print "

User Info

\n"; print "
\n"; print "
\n"; print "
" . Dumper($ui_json) . "
"; print "
\n"; print "
\n"; # Session Management RP iFrame print "\n"; print "\n"; my $logout_redirect_uri = $endsession_uri . "?post_logout_redirect_uri=" . $cgi->url . "&state=" . $state; print "
\n"; print "Logout\n"; print "Logout with redirect\n"; print "
\n"; print "
\n"; print "
url . "\">Home
\n"; } # Configuration read elsif ( $cgi->param("test") eq "configuration" ) { my $openid_configuration_url = $portal_url . "/.well-known/openid-configuration"; print "

Get configuration from $openid_configuration_url

\n"; my $config_response = $ua->get($openid_configuration_url); if ( $config_response->is_error ) { print "
"; print "

Bad configuration response: " . $config_response->message . "

"; print "

$config_response->content

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } my $content = $config_response->decoded_content; my $json; eval { $json = from_json( $content, { allow_nonref => 1 } ); }; if ($@) { print "
"; print "

Wrong JSON content

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } if ( $json->{error} ) { print "
"; print "

Error in configuration response:" . $json->{error} . "

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } print "
\n"; print "
\n"; print "

Configuration content

\n"; print "
\n"; print "
\n"; print "
" . Dumper($json) . "
"; print "
\n"; print "
\n"; if ( $json->{jwks_uri} ) { my $jwks_response = $ua->get( $json->{jwks_uri} ); if ( $jwks_response->is_error ) { print "
"; print "

Bad JWKS response: " . $jwks_response->message . "

"; print "

$jwks_response->content

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } my $jwks_content = $jwks_response->decoded_content; my $jwks_json; eval { $jwks_json = from_json( $jwks_content, { allow_nonref => 1 } ); }; if ($@) { print "
"; print "

Wrong JSON content

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } if ( $json->{error} ) { print "
"; print "

Error in jwks response:" . $json->{error} . "

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } print "
\n"; print "
\n"; print "

JWKS content

\n"; print "
\n"; print "
\n"; print "
" . Dumper($jwks_json) . "
"; print "
\n"; print "
\n"; } print "
url . "\">Home
\n"; } # Registration elsif ( $cgi->param("test") eq "registration" ) { print "

Register fake client on $registration_uri

\n"; my $fake_metadata = { "application_type" => "web", "redirect_uris" => [ "https://client.example.org/callback", "https://client.example.org/callback2" ], "client_name" => "My Example", "client_name#ja-Jpan-JP" => "クライアント名", "logo_uri" => "https://client.example.org/logo.png", "subject_type" => "pairwise", "sector_identifier_uri" => "https://other.example.net/file_of_redirect_uris.json", "token_endpoint_auth_method" => "client_secret_basic", "jwks_uri" => "https://client.example.org/my_public_keys.jwks", "userinfo_encrypted_response_alg" => "RSA1_5", "userinfo_encrypted_response_enc" => "A128CBC-HS256", "contacts" => [ 've7jtb@example.org', 'mary@example.org' ], "request_uris" => [ 'https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA' ], }; print "
\n"; print "
\n"; print "

Client metadata sent

\n"; print "
\n"; print "
\n"; print "
" . Dumper($fake_metadata) . "
"; print "
\n"; print "
\n"; # POST client metadata my $fake_metadata_json = to_json($fake_metadata); my $register_response = $ua->post( $registration_uri, "Content-Type" => 'application/json', "Content" => $fake_metadata_json ); my $register_content = $register_response->decoded_content; my $register_json; eval { $register_json = from_json( $register_content, { allow_nonref => 1 } ); }; if ($@) { print "
"; print "

Wrong JSON content

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } if ( $register_json->{error} ) { print "
"; print "

Error in register response:" . $register_json->{error} . "

"; print "
"; print "
url . "\">Home
\n"; print ""; print ""; print $cgi->end_html(); exit 0; } print "
\n"; print "
\n"; print "

Register content

\n"; print "
\n"; print "
\n"; print "
" . Dumper($register_json) . "
"; print "
\n"; print "
\n"; print "
url . "\">Home
\n"; } # First access else { # AuthN Request my $response_type = uri_escape("code"); my $scope = uri_escape("openid profile address email phone"); my $state = uri_escape("ABCDEFGHIJKLMNOPQRSTUVWXXZ"); my $nonce = uri_escape("1234567890"); my $display = uri_escape(""); # popup my $prompt = uri_escape(""); # login / consent my $ui_locales = uri_escape(""); # fr-FR / en my $login_hint = uri_escape(""); my $max_age = 3600; my $id_token_hint = ""; my $request_paylod_hash = { response_type => "code", scope => "openid profile", client_id => $client_id, redirect_uri => $redirect_uri, display => "page", iss => $client_id, aud => [$portal_url] }; my $request_payload = encode_base64( to_json($request_paylod_hash), "" ); my $request_header_hash = { typ => "JWT", alg => "HS256" }; my $request_header = encode_base64( to_json($request_header_hash), "" ); my $request_digest = hmac_sha256_base64( $request_header . "." . $request_payload, $client_secret ); $request_digest =~ s/\+/-/g; $request_digest =~ s/\//_/g; my $request = uri_escape( $request_header . "." . $request_payload . "." . $request_digest ); my $request_uri = uri_escape($local_request_uri); $client_id = uri_escape($client_id); $redirect_uri = uri_escape($redirect_uri); my $redirect_url = $authorization_uri . "?response_type=$response_type&client_id=$client_id&scope=$scope&redirect_uri=$redirect_uri&state=$state&nonce=$nonce&display=$display&prompt=$prompt&ui_locales=$ui_locales&login_hint=$login_hint&max_age=$max_age&id_token_hint=$id_token_hint"; my $implicit_response_type = uri_escape("id_token token"); my $implicit_redirect_url = $authorization_uri . "?response_type=$implicit_response_type&client_id=$client_id&scope=$scope&redirect_uri=$redirect_uri&state=$state&nonce=$nonce&display=$display&prompt=$prompt&ui_locales=$ui_locales&login_hint=$login_hint&max_age=$max_age&id_token_hint=$id_token_hint"; my $hybrid_response_type = uri_escape("code id_token token"); my $hybrid_redirect_url = $authorization_uri . "?response_type=$hybrid_response_type&client_id=$client_id&scope=$scope&redirect_uri=$redirect_uri&state=$state&nonce=$nonce&display=$display&prompt=$prompt&ui_locales=$ui_locales&login_hint=$login_hint&max_age=$max_age&id_token_hint=$id_token_hint"; my $request_redirect_url = $authorization_uri . "?response_type=code&client_id=$client_id&request=$request&state=$state&scope=openid"; my $request_uri_redirect_url = $authorization_uri . "?response_type=code&client_id=$client_id&request_uri=$request_uri&state=$state&scope=openid"; print "

Authentication

\n"; print "
\n"; print "Authorization Code Flow\n"; print "Implicit Flow\n"; print "Hybrid Flow\n"; print "Authorization Code Flow with request\n"; print "Authorization Code Flow with request_uri\n"; print "
\n"; print "

Configuration

\n"; print "
\n"; print "Configuration discovery\n"; print "
\n"; print "

Registration

\n"; print "
\n"; print "Register a fake client\n"; print "
\n"; } print "\n"; print "\n"; print "\n"; print $cgi->end_html(); exit 0;