File Coverage

blib/lib/OIDC/Client/Plugin.pm
Criterion Covered Total %
statement 297 298 99.6
branch 132 146 90.4
condition 24 36 66.6
subroutine 50 50 100.0
pod 20 20 100.0
total 523 550 95.0


line stmt bran cond sub pod time code
1             package OIDC::Client::Plugin;
2 2     2   1512175 use 5.014;
  2         9  
3 2     2   675 use utf8;
  2         367  
  2         15  
4 2     2   786 use Moose;
  2         589372  
  2         91  
5 2     2   19618 use Moose::Util::TypeConstraints;
  2         6  
  2         24  
6 2     2   6748 use MooseX::Params::Validate;
  2         110005  
  2         21  
7 2     2   1871 use namespace::autoclean;
  2         10017  
  2         14  
8              
9 2     2   188 use Carp qw(croak);
  2         4  
  2         157  
10 2     2   1109 use Module::Load qw(load);
  2         2774  
  2         14  
11 2     2   533 use Mojo::URL;
  2         268589  
  2         31  
12 2     2   125 use Try::Tiny;
  2         4  
  2         158  
13 2     2   1940 use CHI;
  2         127283  
  2         145  
14 2     2   614 use OIDC::Client::AccessToken;
  2         48  
  2         132  
15 2         9 use OIDC::Client::AccessTokenBuilder qw(build_access_token_from_token_response
16 2     2   1344 build_access_token_from_claims);
  2         11  
17 2     2   3215 use OIDC::Client::ApiUserAgentBuilder qw(build_api_useragent_from_access_token);
  2         30  
  2         15  
18 2     2   2603 use OIDC::Client::Identity;
  2         29  
  2         116  
19 2     2   23 use OIDC::Client::Error;
  2         4  
  2         93  
20 2     2   1391 use OIDC::Client::Error::Authentication;
  2         30  
  2         116  
21 2     2   1462 use OIDC::Client::Error::Provider;
  2         33  
  2         13189  
22              
23             with 'OIDC::Client::Role::LoggerWrapper';
24              
25             =encoding utf8
26              
27             =head1 NAME
28              
29             OIDC::Client::Plugin - Main module for the plugins
30              
31             =head1 DESCRIPTION
32              
33             Main module instanciated for the current request by an application plugin
34             (for example: L<Mojolicious::Plugin::OIDC>).
35              
36             It contains all the methods available in the application.
37              
38             =cut
39              
40             enum 'RedirectType' => [qw/login logout/];
41              
42             has 'request_params' => (
43             is => 'ro',
44             isa => 'HashRef',
45             required => 1,
46             );
47              
48             has 'request_headers' => (
49             is => 'ro',
50             isa => 'HashRef',
51             required => 1,
52             );
53              
54             has 'session' => (
55             is => 'ro',
56             isa => 'HashRef',
57             required => 1,
58             traits => ['OIDC::Client::Trait::HashStore'],
59             );
60              
61             has 'stash' => (
62             is => 'ro',
63             isa => 'HashRef',
64             required => 1,
65             traits => ['OIDC::Client::Trait::HashStore'],
66             );
67              
68             has 'redirect' => (
69             is => 'ro',
70             isa => 'CodeRef',
71             required => 1,
72             );
73              
74             has 'client' => (
75             is => 'ro',
76             isa => 'OIDC::Client',
77             required => 1,
78             );
79              
80             has 'base_url' => (
81             is => 'ro',
82             isa => subtype(as 'Str', where { /^http/ || /^$/ }),
83             required => 1,
84             );
85              
86             has 'current_url' => (
87             is => 'ro',
88             isa => 'Str',
89             required => 1,
90             );
91              
92             has 'login_redirect_uri' => (
93             is => 'ro',
94             isa => 'Maybe[Str]',
95             lazy => 1,
96             builder => '_build_login_redirect_uri',
97             );
98              
99             has 'logout_redirect_uri' => (
100             is => 'ro',
101             isa => 'Maybe[Str]',
102             lazy => 1,
103             builder => '_build_logout_redirect_uri',
104             );
105              
106             has 'is_base_url_local' => (
107             is => 'ro',
108             isa => 'Bool',
109             lazy => 1,
110             builder => '_build_is_base_url_local',
111             );
112              
113             has 'audience_cache' => (
114             is => 'ro',
115             isa => 'CHI::Driver',
116             lazy => 1,
117             builder => '_build_audience_cache',
118             );
119              
120             has 'after_touching_session' => (
121             is => 'ro',
122             isa => 'CodeRef',
123             default => sub { sub {} },
124             );
125              
126 63     63   2104 sub _build_is_base_url_local { return shift->base_url =~ m[^https?://localhost\b] }
127 4     4   15 sub _build_login_redirect_uri { return shift->_build_redirect_uri_from_path('login') }
128 2     2   7 sub _build_logout_redirect_uri { return shift->_build_redirect_uri_from_path('logout') }
129              
130             sub _build_redirect_uri_from_path {
131 6     6   8 my $self = shift;
132 6         33 my ($redirect_type) = pos_validated_list(\@_, { isa => 'RedirectType', optional => 0 });
133              
134 6 100       1102 my $redirect_path = $redirect_type eq 'login' ? $self->client->signin_redirect_path
    100          
135             : $self->client->logout_redirect_path
136             or return;
137              
138 2         166 my $base = Mojo::URL->new($self->base_url);
139              
140 2         285 return Mojo::URL->new($redirect_path)->base($base)->to_abs()->to_string();
141             }
142              
143             sub _build_audience_cache {
144 2     2   7 my $self = shift;
145              
146 2         83 my $cache_config = $self->client->cache_config;
147 2         242 my $provider = $self->client->provider;
148              
149 2         172 return CHI->new(%$cache_config, namespace => "OIDC-${provider}-Audience");
150             }
151              
152             after write_session => sub { shift->after_touching_session->() };
153             after delete_session => sub { shift->after_touching_session->() };
154              
155             =head1 METHODS
156              
157             =head2 redirect_to_authorize( %args )
158              
159             $c->oidc->redirect_to_authorize();
160              
161             Redirect the browser to the authorize URL to initiate an authorization code flow.
162             The C<state> parameter contains a generated UUID but other data can be added.
163              
164             The optional hash parameters are:
165              
166             =over 2
167              
168             =item target_url
169              
170             Specifies the URL to redirect to at the end of the authorization code flow.
171             Default to the current URL (attribute C<current_url>).
172              
173             =item redirect_uri
174              
175             Specifies the URL that the provider uses to redirect the user's browser back to
176             the application after the user has been authenticated.
177             Default to the URL built from the C<signin_redirect_path> configuration entry.
178              
179             =item extra_params
180              
181             Hashref which can be used to send extra query parameters.
182              
183             =item other_state_params
184              
185             List (arrayref) of strings to add before the auto-generated UUID string to build
186             the C<state> parameter. All these strings are separated by a comma.
187              
188             =back
189              
190             =cut
191              
192             sub redirect_to_authorize {
193 3     3 1 27 my $self = shift;
194 3         35 my %params = validated_hash(
195             \@_,
196             target_url => { isa => 'Str', optional => 1 },
197             redirect_uri => { isa => 'Str', optional => 1 },
198             extra_params => { isa => 'HashRef', optional => 1 },
199             other_state_params => { isa => 'ArrayRef[Str]', optional => 1 },
200             );
201              
202 3         2237 my $nonce = $self->client->generate_uuid_string();
203 3 100       232 my $state = join ',', (@{$params{other_state_params} || []}, $self->client->generate_uuid_string());
  3         122  
204              
205 3         248 my %args = (
206             nonce => $nonce,
207             state => $state,
208             );
209              
210 3 100 66     164 if (my $redirect_uri = $params{redirect_uri} || $self->login_redirect_uri) {
211 1         4 $args{redirect_uri} = $redirect_uri;
212             }
213              
214 3 100       14 if (my $extra_params = $params{extra_params}) {
215 1         3 $args{extra_params} = $extra_params;
216             }
217              
218 3         183 my $authorize_url = $self->client->auth_url(%args);
219              
220             $self->write_session(['oidc_auth', $state], {
221             nonce => $nonce,
222             provider => $self->client->provider,
223             target_url => $params{target_url} ? $params{target_url}
224 3 100       353 : $self->current_url,
225             });
226              
227 3         36 $self->log_msg(debug => "OIDC: redirecting to provider : $authorize_url");
228 3         328 $self->redirect->($authorize_url);
229             }
230              
231              
232             =head2 get_token( %args )
233              
234             my $identity = $c->oidc->get_token();
235              
236             Checks that the state parameter received from the provider is identical
237             to the state parameter sent with the authorize URL.
238              
239             From a code received from the provider, executes a request to get the token(s).
240              
241             Checks the ID token if present.
242             Checks the access token's C<at_hash> against the ID token's C<at_hash> claim if present.
243              
244             Stores the token(s) in the session, stash or cache depending on your configured
245             C<store_mode> (see L<OIDC::Client::Config>).
246              
247             Returns the stored L<OIDC::Client::Identity> object.
248              
249             The optional hash parameters are:
250              
251             =over 2
252              
253             =item redirect_uri
254              
255             Specifies the URL that the provider uses to redirect the user's browser back to
256             the application after the user has been authenticated.
257             Default to the URL built from the C<signin_redirect_path> configuration entry.
258              
259             =back
260              
261             =cut
262              
263             sub get_token {
264 19     19 1 353 my $self = shift;
265 19         167 my %params = validated_hash(
266             \@_,
267             redirect_uri => { isa => 'Str', optional => 1 },
268             );
269              
270 19         3080 $self->log_msg(debug => 'OIDC: getting token');
271              
272 19         1104 my ($token_response, $auth_data);
273              
274 19 100       679 if ($self->client->token_endpoint_grant_type eq 'authorization_code') {
275 12 100       1317 if ($self->request_params->{error}) {
276 1         43 OIDC::Client::Error::Provider->throw({response_parameters => $self->request_params});
277             }
278 11         39 $auth_data = $self->_extract_auth_data();
279 7   66     81 my $redirect_uri = $params{redirect_uri} || $self->login_redirect_uri;
280             $token_response = $self->client->get_token(
281             code => $self->request_params->{code},
282 7 100       229 $redirect_uri ? (redirect_uri => $redirect_uri) : (),
283             );
284             }
285             else {
286 7         881 $token_response = $self->client->get_token();
287             }
288              
289 14         37 my ($id_token_header, $id_token_claims);
290              
291 14 100 50     540 if (my $id_token = $token_response->id_token) {
    50          
292             ($id_token_header, $id_token_claims) = $self->client->verify_jwt_token(
293             token => $id_token,
294             expected_audience => $self->client->id,
295             expected_authorized_party => $self->client->id,
296             no_authorized_party_accepted => 1,
297             max_token_age => $self->client->max_id_token_age,
298 6 100       163 $auth_data ? (expected_nonce => $auth_data->{nonce}) : (),
299             want_header => 1,
300             );
301 6         822 $self->_store_identity(
302             id_token => $id_token,
303             claims => $id_token_claims,
304             );
305 6         57 $self->log_msg(debug => "OIDC: identity has been stored");
306             }
307             elsif (($self->client->scope // '') =~ /\bopenid\b/) {
308 0         0 OIDC::Client::Error::Authentication->throw(
309             "OIDC: no ID token returned by the provider ?"
310             );
311             }
312              
313 14 100       1344 if ($token_response->access_token) {
314 10         49 my $access_token = build_access_token_from_token_response($token_response);
315             $access_token->verify_at_hash($id_token_claims->{at_hash}, $id_token_header->{alg})
316 10 100       46 if $id_token_claims;
317 9         70 $self->store_access_token($access_token);
318 9         804 $self->log_msg(debug => "OIDC: access token has been stored");
319             }
320              
321 13 100       482 if (my $refresh_token = $token_response->refresh_token) {
322 3         12 $self->store_refresh_token($refresh_token);
323 3         23 $self->log_msg(debug => "OIDC: refresh token has been stored");
324             }
325              
326 13         155 return $self->get_stored_identity();
327             }
328              
329              
330             =head2 refresh_token( $audience_alias )
331              
332             my $stored_access_token = $c->oidc->refresh_token( $audience_alias );
333              
334             Refreshes an access and/or ID token (usually because it has expired) for the default audience
335             (token for the current application) or for the audience corresponding to a given alias
336             (exchanged token for another application).
337              
338             Checks the ID token if it has been renewed.
339             Checks the access token's C<at_hash> against the ID token's C<at_hash> claim if present.
340              
341             Stores the renewed token(s) and returns the new L<OIDC::Client::AccessToken> object.
342              
343             Throws an error if no refresh token has been stored for the audience.
344              
345             The optional list parameters are:
346              
347             =over 2
348              
349             =item audience_alias
350              
351             Alias configured for the audience of the other application.
352              
353             =back
354              
355             =cut
356              
357             sub refresh_token {
358 14     14 1 247 my $self = shift;
359 14         107 my ($audience_alias) = pos_validated_list(\@_, { isa => 'Maybe[Str]', optional => 1 });
360              
361 14 100       3444 my $audience = $audience_alias ? $self->client->get_audience_for_alias($audience_alias)
    100          
362             : $self->client->audience
363             or croak("OIDC: no audience for alias '$audience_alias'");
364              
365 13         1106 $self->log_msg(debug => "OIDC: refreshing token for audience $audience");
366              
367 13 100       823 my $refresh_token = $self->get_stored_refresh_token($audience_alias)
368             or OIDC::Client::Error->throw("OIDC: no refresh token has been stored");
369              
370 12 100       342 my $refresh_scope = $audience_alias ? undef
371             : $self->client->refresh_scope;
372              
373 12 100       1120 my $token_response = $self->client->get_token(
374             grant_type => 'refresh_token',
375             refresh_token => $refresh_token,
376             $refresh_scope ? (refresh_scope => $refresh_scope) : (),
377             );
378              
379 11         31 my ($id_token_header, $id_token_claims);
380              
381 11 100       512 if (my $id_token = $token_response->id_token) {
382 3 50       18 my $identity = $self->get_stored_identity()
383             or OIDC::Client::Error->throw("OIDC: no identity has been stored");
384              
385 3         184 my $expected_subject = $identity->subject;
386 3         160 my $expected_nonce = $identity->claims->{nonce};
387             ($id_token_header, $id_token_claims) = $self->client->verify_jwt_token(
388             token => $id_token,
389             expected_subject => $expected_subject,
390             expected_audience => $self->client->id,
391             expected_authorized_party => $identity->claims->{azp},
392 3 50       126 max_token_age => $self->client->max_id_token_age,
393             $expected_nonce
394             ? (expected_nonce => $expected_nonce,
395             no_nonce_accepted => 1)
396             : (),
397             want_header => 1,
398             );
399 3         615 $self->_store_identity(
400             id_token => $id_token,
401             claims => $id_token_claims,
402             );
403 3         44 $self->log_msg(debug => "OIDC: identity has been renewed and stored");
404             }
405              
406 11 100       470 if ($token_response->access_token) {
407 10         58 my $access_token = build_access_token_from_token_response($token_response);
408             $access_token->verify_at_hash($id_token_claims->{at_hash}, $id_token_header->{alg})
409 10 100       44 if $id_token_claims;
410 9         51 $self->store_access_token($access_token, $audience_alias);
411 9         146 $self->log_msg(debug => "OIDC: access token has been renewed and stored");
412             }
413              
414 10 50       464 if (my $refresh_token = $token_response->refresh_token) {
415 10         52 $self->store_refresh_token($refresh_token, $audience_alias);
416 10         108 $self->log_msg(debug => "OIDC: refresh token has been renewed and stored");
417             }
418              
419 10         639 return $self->get_stored_access_token($audience_alias);
420             }
421              
422              
423             =head2 exchange_token( $audience_alias )
424              
425             my $stored_exchanged_token = $c->oidc->exchange_token($audience_alias);
426              
427             Exchange the access token received during the user's login, for an access token
428             that is accepted by a different OIDC application and stores it.
429              
430             Stores and returns an L<OIDC::Client::AccessToken> object.
431              
432             The list parameters are:
433              
434             =over 2
435              
436             =item audience_alias
437              
438             Alias configured for the audience of the other application.
439              
440             =back
441              
442             =cut
443              
444             sub exchange_token {
445 9     9 1 159 my $self = shift;
446 9         60 my ($audience_alias) = pos_validated_list(\@_, { isa => 'Str', optional => 0 });
447              
448 9         2008 $self->log_msg(debug => 'OIDC: exchanging token');
449              
450 9         473 my $audience = $self->_get_audience_from_alias($audience_alias);
451              
452 8         35 my $access_token = $self->get_valid_access_token();
453              
454 7         154 my $exchanged_token_response = $self->client->exchange_token(
455             token => $access_token->token,
456             audience => $audience,
457             );
458              
459 6         27 my $exchanged_access_token = build_access_token_from_token_response($exchanged_token_response);
460 6         25 $self->store_access_token($exchanged_access_token, $audience_alias);
461 6         59 $self->log_msg(debug => "OIDC: access token has been exchanged and stored");
462              
463 6 100       404 if (my $refresh_token = $exchanged_token_response->refresh_token) {
464 5         18 $self->store_refresh_token($refresh_token, $audience_alias);
465             }
466              
467 6         188 return $exchanged_access_token;
468             }
469              
470              
471             =head2 verify_token()
472              
473             my $access_token = $c->oidc->verify_token();
474              
475             Verifies the access token received in the Authorization header of the current request.
476             Throws an exception if an error occurs. Otherwise, stores an L<OIDC::Client::AccessToken> object
477             and returns it.
478              
479             Depending on the C<token_validation_method> configuration entry, the token is validated
480             either by expecting it is a JWT token or by using the introspection endpoint.
481              
482             To bypass the token verification in local environment, you can configure the C<mocked_access_token>
483             entry (hashref) to be used to create an L<OIDC::Client::AccessToken> object that will be returned
484             by this method.
485              
486             =cut
487              
488             sub verify_token {
489 8     8 1 195 my $self = shift;
490              
491 8 100 66     354 if ($self->is_base_url_local and my $mocked_access_token = $self->client->mocked_access_token) {
492 1         147 return OIDC::Client::AccessToken->new($mocked_access_token);
493             }
494              
495 7 100       30 my $token = $self->get_token_from_authorization_header()
496             or OIDC::Client::Error->throw("OIDC: no token in authorization header");
497              
498 5 100       189 my $claims = $self->client->token_validation_method eq 'jwt'
499             ? $self->client->verify_jwt_token(token => $token)
500             : $self->client->introspect_token(token => $token, token_type_hint => 'access_token');
501              
502 5         403 my $access_token = build_access_token_from_claims($claims, $token);
503 5         29 $self->store_access_token($access_token);
504              
505 5         76 return $access_token;
506             }
507              
508              
509             =head2 get_token_from_authorization_header()
510              
511             my $token = $c->oidc->get_token_from_authorization_header();
512              
513             Returns the token received in the Authorization header of the current request,
514             or returns undef if there is no token in this header.
515              
516             =cut
517              
518             sub get_token_from_authorization_header {
519 9     9 1 31 my $self = shift;
520              
521             my $authorization = $self->request_headers->{Authorization}
522 9 100       379 or return;
523              
524 7         329 my $token_type = $self->client->default_token_type;
525              
526 7         722 my ($token) = $authorization =~ /^$token_type\s+([^\s]+)/i;
527              
528 7         52 return $token;
529             }
530              
531              
532             =head2 get_userinfo()
533              
534             my $userinfo = $c->oidc->get_userinfo();
535              
536             Returns the user informations from the userinfo endpoint.
537              
538             This method should only be invoked when an access token has been stored.
539              
540             To mock the userinfo returned by this method in local environment, you can configure
541             the C<mocked_userinfo> entry (hashref).
542              
543             =cut
544              
545             sub get_userinfo {
546 4     4 1 28 my $self = shift;
547              
548 4 100 66     123 if ($self->is_base_url_local and my $mocked_userinfo = $self->client->mocked_userinfo) {
549 1         52 return $mocked_userinfo;
550             }
551              
552 3         11 my $stored_access_token = $self->get_valid_access_token();
553              
554 3         85 return $self->client->get_userinfo(
555             access_token => $stored_access_token->token,
556             token_type => $stored_access_token->token_type,
557             );
558             }
559              
560              
561             =head2 build_user_from_userinfo( $user_class )
562              
563             my $user = $c->oidc->build_user_from_userinfo();
564              
565             Gets the user informations calling the provider (see L</"get_userinfo()">)
566             and returns a user object (L<OIDC::Client::User> by default) from this user
567             informations.
568              
569             The C<claim_mapping> configuration entry is used to map user information to
570             user attributes.
571              
572             The optional list parameters are:
573              
574             =over 2
575              
576             =item user_class
577              
578             Class to be used to instantiate the user object.
579             Default to L<OIDC::Client::User>.
580              
581             =back
582              
583             =cut
584              
585             sub build_user_from_userinfo {
586 1     1 1 17 my $self = shift;
587 1         14 my ($user_class) = pos_validated_list(\@_, { isa => 'Str', default => 'OIDC::Client::User' });
588              
589 1         568 my $userinfo = $self->get_userinfo();
590              
591 1         7 return $self->build_user_from_claims($userinfo, $user_class);
592             }
593              
594              
595             =head2 build_user_from_claims( $claims, $user_class )
596              
597             my $user = $c->oidc->build_user_from_claims($claims);
598              
599             Returns a user object (L<OIDC::Client::User> by default) based on provided claims.
600              
601             The C<claim_mapping> configuration entry is used to map claim keys to user attributes.
602              
603             This method can be useful, for example, if the access token is a JWT token that has just been
604             verified with the L<verify_token()> method and already contains the relevant information
605             without having to call the C<userinfo> endpoint.
606              
607             The list parameters are:
608              
609             =over 2
610              
611             =item claims
612              
613             Hashref of claims.
614              
615             =item user_class
616              
617             Optional class to be used to instantiate the user object.
618             Default to L<OIDC::Client::User>.
619              
620             =back
621              
622             =cut
623              
624             sub build_user_from_claims {
625 3     3 1 14 my $self = shift;
626 3         22 my ($claims, $user_class) = pos_validated_list(\@_, { isa => 'HashRef', optional => 0 },
627             { isa => 'Str', default => 'OIDC::Client::User' });
628 3         1344 load($user_class);
629              
630 3         338 my $mapping = $self->client->claim_mapping;
631 3         299 my $role_prefix = $self->client->role_prefix;
632              
633             return $user_class->new(
634             (
635             map {
636 3 50       153 my $val = $self->client->get_claim_value(name => $_, claims => $claims, optional => 1);
  15         436  
637 15 100       1754 defined $val ? ($_ => $val) : ();
638             }
639             keys %$mapping
640             ),
641             defined $role_prefix ? (role_prefix => $role_prefix) : (),
642             );
643             }
644              
645              
646             =head2 build_user_from_identity( $user_class )
647              
648             my $user = $c->oidc->build_user_from_identity();
649              
650             Returns a user object (L<OIDC::Client::User> by default) from the claims
651             of the stored identity.
652              
653             The C<claim_mapping> configuration entry is used to map identity claim keys
654             to user attributes.
655              
656             This method should only be invoked when an identity has been stored, i.e.
657             when an authorisation flow has completed and an ID token has been returned
658             by the provider.
659              
660             The optional list parameters are:
661              
662             =over 2
663              
664             =item user_class
665              
666             Class to be used to instantiate the user object.
667             Default to L<OIDC::Client::User>.
668              
669             =back
670              
671             =cut
672              
673             sub build_user_from_identity {
674 1     1 1 9 my $self = shift;
675 1         6 my ($user_class) = pos_validated_list(\@_, { isa => 'Str', default => 'OIDC::Client::User' });
676 1         261 load($user_class);
677              
678 1 50       61 my $identity = $self->get_stored_identity()
679             or OIDC::Client::Error->throw("OIDC: no identity has been stored");
680              
681 1         24 return $self->build_user_from_claims($identity->claims, $user_class);
682             }
683              
684              
685             =head2 build_api_useragent( $audience_alias )
686              
687             my $ua = $c->oidc->build_api_useragent( $audience_alias );
688              
689             Builds a web client (L<Mojo::UserAgent> object) to perform requests
690             on another application with security context propagation.
691              
692             The appropriate access token will be added in the authorization header
693             of each request.
694              
695             The list parameters are:
696              
697             =over 2
698              
699             =item audience_alias
700              
701             Optional alias configured for the audience of the other application.
702             If this parameter is missing, the default audience (current application) is used.
703              
704             =back
705              
706             =cut
707              
708             sub build_api_useragent {
709 4     4 1 54 my $self = shift;
710 4         42 my ($audience_alias) = pos_validated_list(\@_, { isa => 'Str', optional => 1 });
711              
712 4         1411 my $access_token = $self->get_valid_access_token($audience_alias);
713              
714 4         27 return build_api_useragent_from_access_token($access_token);
715             }
716              
717              
718             =head2 redirect_to_logout( %args )
719              
720             $c->oidc->redirect_to_logout();
721              
722             Redirect the browser to the logout URL.
723              
724             The optional hash parameters are:
725              
726             =over 2
727              
728             =item with_id_token
729              
730             Specifies whether the stored id token should be sent to the provider.
731             Default to the C<logout_with_id_token> configuration entry or true by default.
732              
733             =item target_url
734              
735             Specifies the URL to redirect to after the browser is redirected to the logout
736             callback URL.
737              
738             =item post_logout_redirect_uri
739              
740             Specifies the URL that the provider uses to redirect the user's browser back to
741             the application after the logout has been performed.
742             Default to the URL built from the C<logout_redirect_path> configuration entry.
743              
744             =item extra_params
745              
746             Hashref which can be used to send extra query parameters.
747              
748             =item other_state_params
749              
750             List (arrayref) of strings to add before the auto-generated UUID string to build
751             the C<state> parameter. All these strings are separated by a comma.
752              
753             =back
754              
755             =cut
756              
757             sub redirect_to_logout {
758 2     2 1 16 my $self = shift;
759 2         21 my %params = validated_hash(
760             \@_,
761             with_id_token => { isa => 'Bool', optional => 1 },
762             target_url => { isa => 'Str', optional => 1 },
763             post_logout_redirect_uri => { isa => 'Str', optional => 1 },
764             extra_params => { isa => 'HashRef', optional => 1 },
765             other_state_params => { isa => 'ArrayRef[Str]', optional => 1 },
766             );
767              
768 2 100       1291 my $state = join ',', (@{$params{other_state_params} || []}, $self->client->generate_uuid_string());
  2         49  
769              
770 2         111 my %args = (
771             state => $state,
772             );
773              
774 2 100 66     27 if ($params{with_id_token} // $self->client->logout_with_id_token // 1) {
      100        
775 1 50       43 my $identity = $self->get_stored_identity()
776             or OIDC::Client::Error->throw("OIDC: no identity has been stored");
777 1         23 $args{id_token} = $identity->token;
778             }
779              
780 2 50 66     27 if (my $redirect_uri = $params{post_logout_redirect_uri} || $self->logout_redirect_uri) {
781 2         4 $args{post_logout_redirect_uri} = $redirect_uri;
782             }
783              
784 2 100       5 if (my $extra_params = $params{extra_params}) {
785 1         2 $args{extra_params} = $extra_params;
786             }
787              
788 2         38 my $logout_url = $self->client->logout_url(%args);
789              
790             $self->write_session(['oidc_logout', $state], {
791             provider => $self->client->provider,
792             target_url => $params{target_url},
793 2         134 });
794              
795 2         16 $self->log_msg(debug => "OIDC: redirecting to idp for log out : $logout_url");
796 2         147 $self->redirect->($logout_url);
797             }
798              
799              
800             =head2 get_valid_access_token( $audience_alias )
801              
802             my $valid_access_token = $c->oidc->get_valid_access_token( $audience_alias );
803              
804             When an audience alias is specified and no access token has been stored for the audience,
805             returns the execution of the L</"exchange_token( $audience_alias )"> method.
806              
807             Retrieves the stored L<OIDC::Client::AccessToken> object for the default audience
808             (current application) or for the audience corresponding to the given alias.
809              
810             If this token has not expired, returns it, otherwise returns the execution
811             of the L</"refresh_token( $audience_alias )"> method.
812              
813             If the refresh failed and the audience alias is specified, finally returns
814             the execution of the L</"exchange_token( $audience_alias )"> method.
815              
816             The optional list parameters are:
817              
818             =over 2
819              
820             =item audience_alias
821              
822             Alias configured for the audience of the other application.
823              
824             =back
825              
826             In local environment, if the C<mocked_access_token> entry (hashref) is configured,
827             it is used to create an L<OIDC::Client::AccessToken> object that will be returned
828             by this method.
829              
830             =cut
831              
832             sub get_valid_access_token {
833 31     31 1 426 my $self = shift;
834 31         225 my ($audience_alias) = pos_validated_list(\@_, { isa => 'Maybe[Str]', optional => 1 });
835              
836 31         4689 my $stored_access_token = $self->get_stored_access_token($audience_alias);
837              
838 30 100       75 unless ($stored_access_token) {
839 6 100       19 if ($audience_alias) {
840 3         15 return $self->exchange_token($audience_alias);
841             }
842 3         17 $self->get_token();
843 2   66     9 return $self->get_stored_access_token()
844             || OIDC::Client::Error->throw("OIDC: access token has not been retrieved from token endpoint");
845             }
846              
847 24 100       612 my $audience = $audience_alias ? $self->_get_audience_from_alias($audience_alias)
848             : $self->client->audience;
849              
850 24 100       2042 unless ($stored_access_token->expires_at) {
851 12         68 $self->log_msg(debug => "OIDC: no expiration time for the access token with '$audience' audience. Hoping it's still valid.");
852 12         708 return $stored_access_token;
853             }
854              
855 12 100       409 unless ($stored_access_token->has_expired($self->client->expiration_leeway)) {
856 2         14 $self->log_msg(debug => "OIDC: access token for audience '$audience' has been retrieved from store");
857 2         118 return $stored_access_token;
858             }
859              
860 10         79 $self->log_msg(debug => "OIDC: access token has expired for audience '$audience'");
861              
862 10 100       757 if ($self->get_stored_refresh_token($audience_alias)) {
863             my $renewed_access_token = try {
864 7 50   7   577 $self->refresh_token($audience_alias)
865             or OIDC::Client::Error->throw("OIDC: access token has not been refreshed");
866             }
867             catch {
868 1     1   70 $self->log_msg(debug => "OIDC: error refreshing access token for audience '$audience' : $_");
869 1         40 return;
870 7         85 };
871 7 100       404 return $renewed_access_token if $renewed_access_token;
872             }
873              
874 4 100       14 if ($audience_alias) {
875 2         11 return $self->exchange_token($audience_alias);
876             }
877             else {
878 2         12 $self->get_token();
879 1   33     7 return $self->get_stored_access_token()
880             || OIDC::Client::Error->throw("OIDC: access token has not been retrieved from token endpoint");
881             }
882             }
883              
884              
885             =head2 get_valid_identity()
886              
887             my $identity = $c->oidc->get_valid_identity();
888              
889             Executes the L</"get_stored_identity()"> method to get the stored
890             L<OIDC::Client::Identity> object.
891              
892             Returns undef if no identity has been stored or if the stored identity has expired
893             including the configured leeway.
894              
895             Otherwise, returns the stored L<OIDC::Client::Identity> object.
896              
897             =cut
898              
899             sub get_valid_identity {
900 3     3 1 21 my $self = shift;
901              
902 3 100       8 my $stored_identity = $self->get_stored_identity()
903             or return;
904              
905 2 100       41 return if $stored_identity->has_expired($self->client->expiration_leeway);
906              
907 1         3 return $stored_identity;
908             }
909              
910              
911             =head2 get_stored_identity()
912              
913             my $identity = $c->oidc->get_stored_identity();
914              
915             Returns undef if no identity has been stored. Otherwise, returns the stored
916             L<OIDC::Client::Identity> object, even if the identity has expired.
917              
918             By default, the C<expires_at> attribute comes directly from the C<exp> claim but
919             if the C<identity_expires_in> configuration entry is specified, it is added to the current
920             time (when the ID token is retrieved) to force an expiration time.
921              
922             To bypass the OIDC flow in local environment, you can configure the C<mocked_identity>
923             entry (hashref) to be used to create an L<OIDC::Client::Identity> object that will
924             be returned by this method.
925              
926             =cut
927              
928             sub get_stored_identity {
929 26     26 1 94 my $self = shift;
930              
931 26 100 66     933 if ($self->is_base_url_local and my $mocked_identity = $self->client->mocked_identity) {
932 1         162 return OIDC::Client::Identity->new($mocked_identity);
933             }
934              
935 25         752 my $audience = $self->client->id;
936 25 100       1753 my $identity = $self->_get_audience_store($audience, 'identity')
937             or return;
938              
939 14         620 return OIDC::Client::Identity->new($identity);
940             }
941              
942              
943             sub _store_identity {
944 9     9   21 my $self = shift;
945 9         99 my %params = validated_hash(
946             \@_,
947             id_token => { isa => 'Str', optional => 0 },
948             claims => { isa => 'HashRef', optional => 0 },
949             );
950              
951 9         3673 my $subject = $params{claims}->{sub};
952 9 50       39 defined $subject
953             or OIDC::Client::Error::Authentication->throw("OIDC: the 'sub' claim is not defined");
954              
955             my %identity = (
956             subject => $subject,
957             claims => $params{claims},
958             token => $params{id_token},
959 9         42 );
960              
961 9         316 my $expires_in = $self->client->identity_expires_in;
962 9 100       612 if (defined $expires_in) {
963 2 100       8 if ($expires_in != 0) {
964 1         5 $identity{expires_at} = time + $expires_in;
965             }
966             }
967             else {
968             $identity{expires_at} = $params{claims}->{exp}
969 7 50       37 or $self->log_msg(warning => "OIDC: no 'exp' claim in the ID token");
970             }
971              
972 9         300 my $audience = $self->client->id;
973              
974             # not stored as Identity object because we can't rely on the session engine to preserve its type
975 9         569 $self->_set_audience_store($audience, 'identity', \%identity);
976             }
977              
978              
979             =head2 get_stored_access_token( $audience_alias )
980              
981             my $access_token = $c->oidc->get_stored_access_token();
982              
983             Returns the stored L<OIDC::Client::AccessToken> object for the specified audience alias,
984             even if this token has expired.
985              
986             The optional list parameters are:
987              
988             =over 2
989              
990             =item audience_alias
991              
992             Alias configured for the audience of the other application.
993              
994             =back
995              
996             In local environment, if the C<mocked_access_token> entry (hashref) is configured,
997             it is used to create an L<OIDC::Client::AccessToken> object that will be returned
998             by this method.
999              
1000             =cut
1001              
1002             sub get_stored_access_token {
1003 48     48 1 122 my $self = shift;
1004 48         330 my ($audience_alias) = pos_validated_list(\@_, { isa => 'Maybe[Str]', optional => 1 });
1005              
1006 48 100       9573 my $audience = $audience_alias ? $self->_get_audience_from_alias($audience_alias)
1007             : $self->client->audience;
1008              
1009 47 100 66     4171 if ($self->is_base_url_local and my $mocked_access_token = $self->client->mocked_access_token) {
1010 2         154 return OIDC::Client::AccessToken->new($mocked_access_token);
1011             }
1012              
1013 45 100       159 my $access_token = $self->_get_audience_store($audience, 'access_token')
1014             or return;
1015              
1016 37         1539 return OIDC::Client::AccessToken->new($access_token);
1017             }
1018              
1019              
1020             =head2 store_access_token( $access_token, $audience_alias )
1021              
1022             $c->oidc->store_access_token($access_token);
1023              
1024             Stores an L<OIDC::Client::AccessToken> object in the session, stash or cache
1025             depending on your configured C<store_mode> (see L<OIDC::Client::Config>).
1026              
1027             =cut
1028              
1029             sub store_access_token {
1030 31     31 1 75 my $self = shift;
1031 31         247 my ($access_token, $audience_alias) = pos_validated_list(\@_, { isa => 'OIDC::Client::AccessToken', optional => 0 },
1032             { isa => 'Maybe[Str]', optional => 1 });
1033              
1034 31 100       8239 my $audience = $audience_alias ? $self->_get_audience_from_alias($audience_alias)
1035             : $self->client->audience;
1036              
1037             # stored as a hashref because we can't rely on the session engine to preserve the object type
1038 31         1679 $self->_set_audience_store($audience, 'access_token', $access_token->to_hashref);
1039             }
1040              
1041              
1042             =head2 get_stored_refresh_token( $audience_alias )
1043              
1044             my $refresh_token = $c->oidc->get_stored_refresh_token();
1045              
1046             Returns the stored refresh token (string) for the specified audience alias.
1047              
1048             The optional list parameters are:
1049              
1050             =over 2
1051              
1052             =item audience_alias
1053              
1054             Alias configured for the audience of the other application.
1055              
1056             =back
1057              
1058             =cut
1059              
1060             sub get_stored_refresh_token {
1061 23     23 1 44 my $self = shift;
1062 23         146 my ($audience_alias) = pos_validated_list(\@_, { isa => 'Maybe[Str]', optional => 1 });
1063              
1064 23 100       5738 my $audience = $audience_alias ? $self->_get_audience_from_alias($audience_alias)
1065             : $self->client->audience;
1066              
1067 23         1193 return $self->_get_audience_store($audience, 'refresh_token');
1068             }
1069              
1070              
1071             =head2 store_refresh_token( $refresh_token, $audience_alias )
1072              
1073             $c->oidc->store_refresh_token($refresh_token);
1074              
1075             Stores the refresh token value in the session, stash or cache depending
1076             on your configured C<store_mode> (see L<OIDC::Client::Config>).
1077              
1078             =cut
1079              
1080             sub store_refresh_token {
1081 20     20 1 44 my $self = shift;
1082 20         154 my ($refresh_token, $audience_alias) = pos_validated_list(\@_, { isa => 'Str', optional => 0 },
1083             { isa => 'Maybe[Str]', optional => 1 });
1084              
1085 20 100       5627 my $audience = $audience_alias ? $self->_get_audience_from_alias($audience_alias)
1086             : $self->client->audience;
1087              
1088 20         764 $self->_set_audience_store($audience, 'refresh_token', $refresh_token);
1089             }
1090              
1091              
1092             sub _get_audience_from_alias {
1093 59     59   104 my $self = shift;
1094 59         243 my ($audience_alias) = pos_validated_list(\@_, { isa => 'Str', optional => 0 });
1095              
1096 59 100       11859 my $audience = $self->client->get_audience_for_alias($audience_alias)
1097             or croak("OIDC: no audience for alias '$audience_alias'");
1098              
1099 57         4205 return $audience;
1100             }
1101              
1102              
1103             sub _extract_auth_data {
1104 11     11   20 my $self = shift;
1105              
1106             my $state = $self->request_params->{state}
1107 11 100       300 or OIDC::Client::Error::Authentication->throw("OIDC: no state parameter in request");
1108              
1109 8 100       38 my $auth_data = $self->delete_session(['oidc_auth', $state])
1110             or OIDC::Client::Error::Authentication->throw("OIDC: no authorisation data for state : '$state'");
1111              
1112 7         60 return $auth_data;
1113             }
1114              
1115              
1116             sub _get_audience_store {
1117 93     93   236 my ($self, $audience, $key) = @_;
1118              
1119 93         3223 my $store_mode = $self->client->store_mode;
1120              
1121 93         5956 my $audience_store;
1122 93 100       246 if ($store_mode eq 'cache') {
1123 1         49 $audience_store = $self->audience_cache->get($audience);
1124             }
1125             else {
1126 92 50       251 my $meth = $store_mode eq 'session' ? 'read_session' : 'read_stash';
1127 92         3163 my @path = ('oidc', 'provider', $self->client->provider, 'audience', $audience);
1128 92         5781 $audience_store = $self->$meth(\@path);
1129             }
1130              
1131 93   100     614 $audience_store ||= {};
1132 93 50       980 return $key ? $audience_store->{$key} : $audience_store;
1133             }
1134              
1135              
1136             sub _set_audience_store {
1137 60     60   161 my ($self, $audience, $key, $value) = @_;
1138              
1139 60         1876 my $store_mode = $self->client->store_mode;
1140              
1141 60 100       3740 if ($store_mode eq 'cache') {
1142 1         49 my $audience_store = $self->audience_cache->get($audience);
1143 1         270 $audience_store->{$key} = $value;
1144 1         52 $self->audience_cache->set($audience, $audience_store);
1145             }
1146             else {
1147 59 100       145 my $meth = $store_mode eq 'session' ? 'write_session' : 'write_stash';
1148 59         1976 my @path = ('oidc', 'provider', $self->client->provider, 'audience', $audience, $key);
1149 59         3584 $self->$meth(\@path, $value);
1150             }
1151             }
1152              
1153              
1154             =head2 delete_stored_data()
1155              
1156             $c->oidc->delete_stored_data();
1157              
1158             Delete the tokens and other data stored in the session, stash or cache
1159             depending on your configured C<store_mode> (see L<OIDC::Client::Config>).
1160              
1161             Note that only the data from the current provider is deleted.
1162              
1163             =cut
1164              
1165             sub delete_stored_data {
1166 3     3 1 33 my $self = shift;
1167              
1168 3         103 my $store_mode = $self->client->store_mode;
1169              
1170 3 100       274 if ($store_mode eq 'cache') {
1171 1         51 $self->audience_cache->clear();
1172             }
1173             else {
1174 2 50       8 my $meth = $store_mode eq 'session' ? 'delete_session' : 'delete_stash';
1175 2         69 my @path = ('oidc', 'provider', $self->client->provider);
1176 2         127 $self->$meth(\@path);
1177             }
1178              
1179 3         30 return;
1180             }
1181              
1182              
1183             __PACKAGE__->meta->make_immutable;
1184              
1185             1;