File Coverage

blib/lib/OIDC/Client.pm
Criterion Covered Total %
statement 204 205 99.5
branch 89 100 89.0
condition 47 60 78.3
subroutine 32 32 100.0
pod 14 14 100.0
total 386 411 93.9


line stmt bran cond sub pod time code
1             package OIDC::Client;
2 2     2   538976 use 5.020;
  2         9  
3 2     2   722 use utf8;
  2         423  
  2         16  
4 2     2   906 use Moose;
  2         643694  
  2         15  
5 2     2   18177 use Moose::Util::TypeConstraints;
  2         11  
  2         31  
6 2     2   5563 use MooseX::Params::Validate;
  2         95238  
  2         14  
7 2     2   1581 use namespace::autoclean;
  2         9207  
  2         11  
8              
9 2     2   170 use Carp qw(croak);
  2         3  
  2         138  
10 2     2   10 use List::Util qw(first);
  2         5  
  2         117  
11 2     2   9 use Try::Tiny;
  2         2  
  2         116  
12 2     2   1615 use Crypt::JWT ();
  2         84071  
  2         118  
13 2     2   936 use Mojo::URL;
  2         238218  
  2         23  
14 2     2   1393 use OIDC::Client::Error::TokenValidation;
  2         38  
  2         140  
15 2         13 use OIDC::Client::ApiUserAgentBuilder qw(build_api_useragent_from_token_response
16 2     2   1301 build_api_useragent_from_token_value);
  2         13  
17 2     2   3349 use OIDC::Client::Utils qw(reach_data);
  2         10  
  2         9  
18              
19             our $VERSION = '1.06'; # VERSION: generated by Dist::Zilla::Plugin::OurPkgVersion
20              
21             =encoding utf8
22              
23             =head1 NAME
24              
25             OIDC::Client - OpenID Connect Client
26              
27             =head1 SYNOPSIS
28              
29             my $client = OIDC::Client->new(
30             provider => 'my_provider',
31             id => 'my_client_id',
32             secret => 'my_client_secret',
33             provider_metadata => \%provider_metadata,
34             log => $app->log,
35             );
36              
37             # or...
38              
39             my $client = OIDC::Client->new(
40             config => $config_provider,
41             log => $app->log,
42             );
43              
44             my $authorize_url = $client->auth_url();
45             my $token_response = $client->get_token(code => $code);
46             my $claims = $client->verify_jwt_token(token => $token);
47             my $claims = $client->introspect_token(token => $token);
48             my $userinfo = $client->get_userinfo(access_token => $token);
49             my $token_response = $client->exchange_token(token => $token, audience => $audience);
50             my $ua = $client->build_api_useragent();
51             my $logout_url = $client->logout_url();
52              
53             =head1 DESCRIPTION
54              
55             Client module for OpenID Connect and OAuth 2.0 protocols.
56              
57             Use this module directly from a batch or a simple script. For use from within
58             an application, you should instead use one of these framework plugins, which
59             all use this distribution :
60              
61             =over 2
62              
63             =item * L<Mojolicious::Plugin::OIDC>
64              
65             =item * L<Catalyst::Plugin::OIDC>
66              
67             =item * L<Dancer2::Plugin::OIDC>
68              
69             =back
70              
71             =cut
72              
73             enum 'StoreMode' => [qw/session stash cache/];
74             enum 'ResponseMode' => [qw/query form_post/];
75             enum 'GrantType' => [qw/authorization_code client_credentials password refresh_token/];
76             enum 'ClientAuthMethod' => [qw/client_secret_basic client_secret_post client_secret_jwt private_key_jwt none/];
77             enum 'TokenValidationMethod' => [qw/jwt introspection/];
78              
79             with 'OIDC::Client::Role::LoggerWrapper';
80             with 'OIDC::Client::Role::AttributesManager';
81             with 'OIDC::Client::Role::ConfigurationChecker';
82             with 'OIDC::Client::Role::ClaimsValidator';
83             with 'OIDC::Client::Role::ClientAuthenticationHelper';
84              
85             =head1 METHODS
86              
87             =head2 BUILD
88              
89             Called after the object is created. Makes some basic checks and forces
90             the retrieval of provider metadata and kid keys.
91              
92             =cut
93              
94             sub BUILD {
95 61     61 1 126 my $self = shift;
96              
97 61         327 $self->_check_configuration();
98 60         66687 $self->_check_audiences_configuration();
99 58         1283 $self->_check_cache_configuration();
100              
101 57         2138 $self->provider;
102 56         1911 $self->id;
103              
104 56         1956 $self->provider_metadata;
105 56 100       1801 $self->kid_keys if $self->provider_metadata->{jwks_url};
106             }
107              
108              
109             =head2 auth_url( %args )
110              
111             my $authorize_url = $client->auth_url(%args);
112              
113             Returns a scalar or a L<Mojo::URL> object containing the initial authorization URL.
114             This is the URL to use to initiate an authorization code flow.
115              
116             The optional parameters are:
117              
118             =over 2
119              
120             =item response_mode
121              
122             Defines how tokens are sent by the provider.
123              
124             Can take one of these values:
125              
126             =over 2
127              
128             =item query
129              
130             Tokens are sent in query parameters.
131              
132             =item form_post
133              
134             Tokens are sent in a POST form.
135              
136             =back
137              
138             =item redirect_uri
139              
140             Redirection URI to which the response will be sent.
141             Can also be specified in the C<signin_redirect_uri> configuration entry.
142              
143             =item state
144              
145             String which is sent during the request to the identity provider
146             and sent back from the IDP along with the Code.
147              
148             =item nonce
149              
150             String which is sent during the request to the identity provider
151             and sent back from the IDP in the returned ID Token.
152              
153             =item scope
154              
155             Specifies the desired scope of the requested token.
156             Must be a string with space separators.
157             Can also be specified in the C<scope> configuration entry.
158              
159             =item audience
160              
161             Specifies the audience/resource that the access token is intended for.
162             Can also be specified in the C<audience> configuration entry.
163              
164             =item extra_params
165              
166             Hashref which can be used to send extra query parameters.
167             Can also be specified in the C<authorize_endpoint_extra_params> configuration entry.
168              
169             =item want_mojo_url
170              
171             Defines whether you want this method to return a L<Mojo::URL> object
172             instead of a scalar. False by default.
173              
174             =back
175              
176             =cut
177              
178             sub auth_url {
179 4     4 1 4246 my $self = shift;
180 4         48 my (%params) = validated_hash(
181             \@_,
182             response_mode => { isa => 'ResponseMode', optional => 1 },
183             redirect_uri => { isa => 'Str', optional => 1 },
184             state => { isa => 'Str', optional => 1 },
185             nonce => { isa => 'Str', optional => 1 },
186             scope => { isa => 'Str', optional => 1 },
187             audience => { isa => 'Str', optional => 1 },
188             extra_params => { isa => 'HashRef', optional => 1 },
189             want_mojo_url => { isa => 'Bool', default => 0 },
190             );
191              
192             my $authorize_url = $self->provider_metadata->{authorize_url}
193 4 100       3750 or croak("OIDC: authorize url not found in provider metadata");
194              
195 3         102 my %args = (
196             response_type => 'code',
197             client_id => $self->id,
198             );
199              
200 3 50 33     106 if (my $response_mode = $params{response_mode} || $self->authorize_endpoint_response_mode) {
201 0         0 $args{response_mode} = $response_mode;
202             }
203              
204 3 100 100     73 if (my $redirect_uri = $params{redirect_uri} || $self->signin_redirect_uri) {
205 2         8 $args{redirect_uri} = $redirect_uri;
206             }
207              
208 3         6 foreach my $param_name (qw/state nonce/) {
209 6 100       23 if (defined $params{$param_name}) {
210 1         3 $args{$param_name} = $params{$param_name};
211             }
212             }
213              
214 3 100 100     81 if (my $scope = $params{scope} || $self->scope) {
215 2         17 $args{scope} = $scope;
216             }
217              
218 3 100 100     82 if (my $audience = $params{audience} || ($self->audience ne $self->id ? $self->audience : undef)) {
219 2         5 $args{audience} = $audience;
220             }
221              
222 3 100 100     101 if (my $extra_params = $params{extra_params} || $self->authorize_endpoint_extra_params) {
223 2         9 foreach my $param_name (keys %$extra_params) {
224 3         8 $args{$param_name} = $extra_params->{$param_name};
225             }
226             }
227              
228 3         28 my $auth_url = Mojo::URL->new($authorize_url);
229 3         454 $auth_url->query(%args);
230              
231 3 100       278 return $params{want_mojo_url} ? $auth_url : $auth_url->to_string;
232             }
233              
234              
235             =head2 get_token( %args )
236              
237             my $token_response = $client->get_token(
238             code => $code,
239             redirect_uri => q{http://yourapp/oidc/callback},
240             );
241              
242             Fetch token(s) from an OAuth2/OIDC provider and returns an
243             L<OIDC::Client::TokenResponse> object.
244              
245             This method doesn't perform any verification on the ID token.
246             Call the L</"verify_jwt_token( %args )"> method to do so.
247              
248             The optional parameters are:
249              
250             =over 2
251              
252             =item grant_type
253              
254             Specifies how the client wants to interact with the identity provider.
255             Accepted here : C<authorization_code>, C<client_credentials>, C<password>
256             or C<refresh_token>.
257             Can also be specified in the C<token_endpoint_grant_type> configuration entry.
258             Default to C<authorization_code>.
259              
260             =item auth_method
261              
262             Specifies how the client authenticates with the identity provider.
263              
264             Supported client authentication methods:
265              
266             =over 2
267              
268             =item *
269              
270             client_secret_basic (default)
271              
272             =item *
273              
274             client_secret_post
275              
276             =item *
277              
278             client_secret_jwt
279              
280             =item *
281              
282             private_key_jwt
283              
284             =item *
285              
286             none
287              
288             =back
289              
290             Can also be specified in the C<token_endpoint_auth_method> configuration entry
291             or the global C<client_auth_method> configuration entry.
292             Default to C<client_secret_basic>.
293              
294             =item code
295              
296             Authorization-code that is issued beforehand by the identity provider.
297             Used only for the C<authorization_code> grant-type.
298              
299             =item redirect_uri
300              
301             Redirection URI to which the response will be sent.
302             Can also be specified in the C<signin_redirect_uri> configuration entry.
303             Used only for the C<authorization_code> grant-type.
304              
305             =item username / password
306              
307             User credentials for authorization
308             Can also be specified in the C<username> and C<password> configuration entries.
309             Used only the for C<password> grant-type.
310              
311             =item audience
312              
313             Specifies the Relaying Party for which the token is intended.
314             Can also be specified in the C<audience> configuration entry.
315             Not used for the C<refresh_token> grant-type.
316              
317             =item scope
318              
319             Specifies the desired scope of the requested token.
320             Must be a string with space separators.
321             Can also be specified in the C<scope> configuration entry.
322             Not used for the C<authorization_code> nor the C<refresh_token> grant-type.
323              
324             =item refresh_token
325              
326             Token that can be used to renew the associated access token before it expires.
327             Used only for the C<refresh_token> grant-type.
328              
329             =item refresh_scope
330              
331             Specifies the desired scope of the requested renewed token.
332             Must be a string with space separators.
333             Used only for the C<refresh_token> grant-type.
334              
335             =back
336              
337             =cut
338              
339             sub get_token {
340 15     15 1 155 my $self = shift;
341 15         255 my (%params) = validated_hash(
342             \@_,
343             grant_type => { isa => 'GrantType', optional => 1 },
344             auth_method => { isa => 'ClientAuthMethod', optional => 1 },
345             code => { isa => 'Str', optional => 1 },
346             redirect_uri => { isa => 'Str', optional => 1 },
347             username => { isa => 'Str', optional => 1 },
348             password => { isa => 'Str', optional => 1 },
349             audience => { isa => 'Str', optional => 1 },
350             scope => { isa => 'Str', optional => 1 },
351             refresh_token => { isa => 'Str', optional => 1 },
352             refresh_scope => { isa => 'Str', optional => 1 },
353             );
354              
355 15   66     8153 my $grant_type = $params{grant_type} || $self->token_endpoint_grant_type;
356              
357             my $token_url = $self->provider_metadata->{token_url}
358 15 100       537 or croak("OIDC: token url not found in provider metadata");
359              
360 14   33     548 my $auth_method = $params{auth_method} || $self->token_endpoint_auth_method;
361 14         111 my ($headers, $form) = $self->_build_client_auth_arguments($auth_method, $token_url);
362              
363 14         37 $form->{grant_type} = $grant_type;
364              
365 14 100       71 if ($grant_type eq 'authorization_code') {
    100          
    100          
366             $form->{code} = $params{code}
367 5 50       19 or croak("OIDC: code is missing");
368              
369 5 50 66     130 if (my $redirect_uri = $params{redirect_uri} || $self->signin_redirect_uri) {
370 5         12 $form->{redirect_uri} = $redirect_uri;
371             }
372             }
373             elsif ($grant_type eq 'password') {
374 3         8 foreach my $required_field (qw/username password/) {
375 6 50 66     155 $form->{$required_field} = ($params{$required_field} || $self->$required_field)
376             or croak("OIDC: $required_field is missing");
377             }
378             }
379             elsif ($grant_type eq 'refresh_token') {
380             $form->{refresh_token} = $params{refresh_token}
381 4 50       17 or croak("OIDC: refresh_token is missing");
382              
383             # Only the 'refresh_scope' method parameter should be considered here. The configuration
384             # parameter is used by Plugin.pm which knows for which audience the token is renewed
385 4 100       17 if (my $scope = $params{refresh_scope}) {
386 1         6 $form->{scope} = $scope;
387             }
388             }
389              
390 14 100       73 unless ($grant_type =~ /^(authorization_code|refresh_token)$/) {
391 5 50 66     127 if (my $scope = ($params{scope} || $self->scope)) {
392 5         11 $form->{scope} = $scope;
393             }
394             }
395              
396 14 100       35 unless ($grant_type eq 'refresh_token') {
397 10 100 100     269 if (my $audience = $params{audience} || ($self->audience ne $self->id ? $self->audience : undef)) {
398 7         16 $form->{audience} = $audience;
399             }
400             }
401              
402 14         77 $self->log_msg(debug => 'OIDC: calling provider to get token');
403              
404 14         1498 my $res = $self->user_agent->post($token_url, $headers, form => $form)->result;
405              
406 14         2669 return $self->token_response_parser->parse($res);
407             }
408              
409              
410             =head2 verify_jwt_token( %args )
411              
412             my $claims = $client->verify_jwt_token(
413             token => $token,
414             expected_audience => $audience,
415             expected_nonce => $nonce,
416             );
417              
418             Checks the structure, claims and signature of the JWT token.
419             Throws an L<OIDC::Client::Error::TokenValidation> exception if an error occurs.
420             Otherwise, returns the claims (hashref).
421              
422             This method automatically manages a JWK key rotation. If a JWK key error
423             is detected during token verification, the JWK keys in memory are refreshed
424             by retrieving them again from the JWKS URL. The token is checked again, and if
425             an error occurs, an L<OIDC::Client::Error::TokenValidation> exception is thrown.
426              
427             The following claims are validated :
428              
429             =over 2
430              
431             =item "exp" (Expiration Time) claim
432              
433             Must be present and valid (in the future).
434              
435             =item "iat" (Issued At) claim
436              
437             Must be present and valid (not in the future).
438              
439             =item "nbf" (Not Before) claim
440              
441             Must be valid (not in the future) if present.
442              
443             =item "iss" (Issuer) claim
444              
445             Must be the issuer recorded in the provider metadata.
446              
447             =item "aud" (Audience) claim
448              
449             Must be the expected audience (see parameters beelow).
450              
451             =item "sub" (Subject) claim
452              
453             Must be the expected subject defined in the parameters (see beelow).
454              
455             =back
456              
457             The L<Crypt::JWT::decode_jwt()|https://metacpan.org/pod/Crypt::JWT#decode_jwt>
458             function is used to validate and decode a JWT token. Remember that you can change
459             the options transmitted to this function (see L<OIDC::Client::Config>).
460              
461             The parameters are:
462              
463             =over 2
464              
465             =item token
466              
467             The JWT token to validate.
468             Required.
469              
470             =item expected_audience
471              
472             If the token is not intended for the expected audience, an exception is thrown.
473             Default to the C<audience> configuration entry or otherwise the client id.
474              
475             =item expected_authorized_party
476              
477             If the C<azp> claim value is not the expected authorized_party, an exception is thrown
478             unless the JWT token has no C<azp> claim and the C<no_authorized_party_accepted>
479             parameter is true.
480             Optional.
481              
482             =item no_authorized_party_accepted
483              
484             When the C<expected_authorized_party> parameter is defined, prevents an exception
485             from being thrown if the JWT token does not contain an C<azp> claim.
486             Default to false.
487              
488             =item expected_subject
489              
490             If the C<sub> claim value is not the expected subject, an exception is thrown.
491             Optional.
492              
493             =item expected_nonce
494              
495             If the C<nonce> claim value is not the expected nonce, an exception is thrown
496             unless the JWT token has no C<nonce> claim and the C<no_nonce_accepted> parameter is true.
497             Optional.
498              
499             =item no_nonce_accepted
500              
501             When the C<expected_nonce> parameter is defined, prevents an exception from being
502             thrown if the JWT token does not contain a C<nonce> claim.
503             Default to false.
504              
505             =item want_header
506              
507             my ($header, $claims) = $client->verify_jwt_token(
508             token => $token,
509             want_header => 1,
510             );
511              
512             Defines whether you want the decoded header to be returned by this method.
513             False by default.
514              
515             =back
516              
517             =cut
518              
519             sub verify_jwt_token {
520 26     26 1 73315 my $self = shift;
521 26         310 my (%params) = validated_hash(
522             \@_,
523             token => { isa => 'Str', optional => 0 },
524             expected_audience => { isa => 'Str', optional => 1 },
525             expected_authorized_party => { isa => 'Maybe[Str]', optional => 1 },
526             no_authorized_party_accepted => { isa => 'Bool', default => 0 },
527             expected_subject => { isa => 'Str', optional => 1 },
528             expected_nonce => { isa => 'Str', optional => 1 },
529             no_nonce_accepted => { isa => 'Bool', default => 0 },
530             max_token_age => { isa => 'Int', optional => 1 },
531             want_header => { isa => 'Bool', default => 0 },
532             );
533              
534             # checks the signature and the timestamps
535 26         10718 my ($header, $claims) = $self->_decode_token($params{token});
536              
537             # checks the issuer
538 24         744 $self->_validate_issuer($claims->{iss});
539              
540             # checks the audience
541 22         123 $self->_validate_audience($claims->{aud}, $params{expected_audience});
542              
543             # checks the authorized party
544 19 100       78 if (exists $params{expected_authorized_party}) {
545 6 100 100     21 unless (!exists $claims->{azp} && $params{no_authorized_party_accepted}) {
546 5         28 $self->_validate_authorized_party($claims->{azp}, $params{expected_authorized_party});
547             }
548             }
549              
550             # checks the subject
551 16 100       47 if (my $expected_subject = $params{expected_subject}) {
552 3         10 $self->_validate_subject($claims->{sub}, $expected_subject);
553             }
554              
555             # checks the nonce
556 14 100       38 if (my $expected_nonce = $params{expected_nonce}) {
557 4 100 100     22 unless (!exists $claims->{nonce} && $params{no_nonce_accepted}) {
558 3         12 $self->_validate_nonce($claims->{nonce}, $expected_nonce);
559             }
560             }
561              
562             # checks that the token has not been issued too far away from the current time
563 12 100       36 if (my $max_token_age = $params{max_token_age}) {
564 2         11 $self->_validate_age($claims->{iat}, $max_token_age);
565             }
566              
567 11 100       76 return $params{want_header} ? ($header, $claims) : $claims;
568             }
569              
570             =head2 verify_token( %args )
571              
572             This method is DEPRECATED! Instead, use L</"verify_jwt_token( %args )">.
573              
574             =cut
575              
576             sub verify_token {
577 1     1 1 2202 my ($self, %params) = @_;
578 1         36 warnings::warnif('deprecated',
579             'OIDC::Client::verify_token() is deprecated in favor of OIDC::Client::verify_jwt_token()');
580 1         1241 return $self->verify_jwt_token(%params);
581             }
582              
583              
584             =head2 introspect_token( %args )
585              
586             my $claims = $client->introspect_token(
587             token => $token,
588             token_type_hint => 'access_token',
589             );
590              
591             Allows a Resource Server to validate a token and obtain its metadata by calling the provider's
592             introspection endpoint. Typically used when the access token is opaque (not a JWT).
593              
594             Throws an L<OIDC::Client::Error::Provider> exception if an error is returned by the provider
595             or an L<OIDC::Client::Error::TokenValidation> exception if a validation error occurs.
596             Otherwise, returns the claims.
597              
598             The parameters are:
599              
600             =over 2
601              
602             =item token
603              
604             The token to validate.
605             Required.
606              
607             =item token_type_hint
608              
609             Hint about the type of the token submitted for introspection.
610              
611             =item auth_method
612              
613             Specifies how the client authenticates with the identity provider.
614              
615             Supported client authentication methods:
616              
617             =over 2
618              
619             =item *
620              
621             client_secret_basic (default)
622              
623             =item *
624              
625             client_secret_post
626              
627             =item *
628              
629             client_secret_jwt
630              
631             =item *
632              
633             private_key_jwt
634              
635             =item *
636              
637             none
638              
639             =back
640              
641             Can also be specified in the C<introspection_endpoint_auth_method> configuration entry
642             or the global C<client_auth_method> configuration entry.
643             Default to C<client_secret_basic>.
644              
645             =item expected_audience
646              
647             If the C<aud> claim is present in the provider response of the introspection endpoint,
648             its value must match the expected audience, otherwise an exception is thrown.
649             Default to the C<audience> configuration entry or the client id.
650              
651             =back
652              
653             =cut
654              
655             sub introspect_token {
656 7     7 1 220 my $self = shift;
657 7         108 my (%params) = validated_hash(
658             \@_,
659             token => { isa => 'Str', optional => 0 },
660             token_type_hint => { isa => enum([qw/access_token refresh_token/]), optional => 1 },
661             auth_method => { isa => 'ClientAuthMethod', optional => 1 },
662             expected_audience => { isa => 'Str', optional => 1 },
663             );
664              
665             my $introspection_url = $self->provider_metadata->{introspection_url}
666 7 50       19066 or croak("OIDC: introspection_url not found in provider metadata");
667              
668 7   33     366 my $auth_method = $params{auth_method} || $self->introspection_endpoint_auth_method;
669 7         49 my ($headers, $form) = $self->_build_client_auth_arguments($auth_method, $introspection_url);
670              
671 7         25 $form->{token} = $params{token};
672              
673 7 100       28 if (my $token_type_hint = $params{token_type_hint}) {
674 1         4 $form->{token_type_hint} = $token_type_hint;
675             }
676              
677 7         43 $self->log_msg(debug => 'OIDC: calling provider to introspect token');
678 7         870 my $res = $self->user_agent->post($introspection_url, $headers, form => $form)->result;
679              
680 7         1564 my $claims = $self->response_parser->parse($res);
681              
682             $claims->{active}
683 7 100       540 or OIDC::Client::Error::TokenValidation->throw("OIDC: inactive token");
684              
685 6 100       21 if (exists $claims->{iss}) {
686 3         20 $self->_validate_issuer($claims->{iss});
687             }
688              
689 5 100       18 if (exists $claims->{aud}) {
690 2         12 $self->_validate_audience($claims->{aud}, $params{expected_audience});
691             }
692              
693 4         25 return $claims;
694             }
695              
696              
697             =head2 get_userinfo( %args )
698              
699             my $userinfo = $client->get_userinfo(
700             access_token => $stored_token->token,
701             token_type => $stored_token->token_type,
702             );
703              
704             Get and returns the user information from an OAuth2/OIDC provider.
705              
706             The parameters are:
707              
708             =over 2
709              
710             =item access_token
711              
712             Content of the valid access token obtained through OIDC authentication.
713              
714             =item token_type
715              
716             Optional, default to C<Bearer>.
717              
718             =back
719              
720             =cut
721              
722             sub get_userinfo {
723 2     2 1 7433 my $self = shift;
724 2         19 my (%params) = validated_hash(
725             \@_,
726             access_token => { isa => 'Str', optional => 0 },
727             token_type => { isa => 'Maybe[Str]', optional => 1 },
728             );
729              
730             my $userinfo_url = $self->provider_metadata->{userinfo_url}
731 2 50       885 or croak("OIDC: userinfo_url not found in provider metadata");
732              
733 2   66     29 my $token_type = $params{token_type} || $self->default_token_type;
734              
735 2         5 my $authorization = "$token_type $params{access_token}";
736              
737 2         10 $self->log_msg(debug => 'OIDC: calling provider to fetch userinfo');
738              
739 2         156 my $res = $self->user_agent->get($userinfo_url, { Authorization => $authorization })
740             ->result;
741              
742 2         248 return $self->response_parser->parse($res);
743             }
744              
745              
746             =head2 get_audience_for_alias( $audience_alias )
747              
748             my $audience = $client->get_audience_for_alias($audience_alias);
749              
750             Returns the audience for an alias that has been configured in the configuration
751             entry C<audience_alias>/C<$audience_alias>/C<audience>.
752              
753             =cut
754              
755             sub get_audience_for_alias {
756 2     2 1 709 my $self = shift;
757 2         31 my ($alias) = pos_validated_list(\@_, { isa => 'Str', optional => 0 });
758              
759 2 50       523 my $audience_alias = $self->audience_alias
760             or return;
761              
762 2 100       7 my $audience_infos = $audience_alias->{$alias}
763             or return;
764              
765 1         3 return $audience_infos->{audience};
766             }
767              
768              
769             =head2 get_scope_for_audience( $audience )
770              
771             my $scope = $client->get_scope_for_audience($audience);
772              
773             Returns the scope for an audience that has been configured in the configuration
774             entry C<audience_alias>/C<$audience_alias>/C<scope>.
775              
776             =cut
777              
778             sub get_scope_for_audience {
779 6     6 1 515 my $self = shift;
780 6         38 my ($audience) = pos_validated_list(\@_, { isa => 'Str', optional => 0 });
781              
782 6 100       1230 my $audience_alias = $self->audience_alias
783             or return;
784              
785 5 50   6   27 my $audience_infos = first { $_->{audience} eq $audience } values %$audience_alias
  6         16  
786             or return;
787              
788 5         22 return $audience_infos->{scope};
789             }
790              
791              
792             =head2 exchange_token( %args )
793              
794             my $exchanged_token_response = $client->exchange_token(
795             token => $token,
796             audience => $audience,
797             );
798              
799             Exchanges an access token, obtained through OIDC authentication, for another access
800             token that is accepted by a different OIDC application.
801              
802             Returns a L<OIDC::Client::TokenResponse> object.
803              
804             The parameters are:
805              
806             =over 2
807              
808             =item token
809              
810             Content of the valid access token obtained through OIDC authentication.
811              
812             =item audience
813              
814             Audience of the target application.
815              
816             =item scope
817              
818             Specifies the desired scope of the requested token.
819             Must be a string with space separators.
820             Optional.
821              
822             =item auth_method
823              
824             Specifies how the client authenticates with the identity provider.
825              
826             Supported client authentication methods:
827              
828             =over 2
829              
830             =item *
831              
832             client_secret_basic (default)
833              
834             =item *
835              
836             client_secret_post
837              
838             =item *
839              
840             client_secret_jwt
841              
842             =item *
843              
844             private_key_jwt
845              
846             =item *
847              
848             none
849              
850             =back
851              
852             Can also be specified in the C<token_endpoint_auth_method> configuration entry
853             or the global C<client_auth_method> configuration entry.
854             Default to C<client_secret_basic>.
855              
856             =back
857              
858             =cut
859              
860             sub exchange_token {
861 5     5 1 28 my $self = shift;
862 5         40 my (%params) = validated_hash(
863             \@_,
864             token => { isa => 'Str', optional => 0 },
865             audience => { isa => 'Str', optional => 0 },
866             scope => { isa => 'Str', optional => 1 },
867             auth_method => { isa => 'ClientAuthMethod', optional => 1 },
868             );
869              
870             my $token_url = $self->provider_metadata->{token_url}
871 5 50       1784 or croak("OIDC: token url not found in provider metadata");
872              
873 5   33     124 my $auth_method = $params{auth_method} || $self->token_endpoint_auth_method;
874 5         26 my ($headers, $form) = $self->_build_client_auth_arguments($auth_method, $token_url);
875              
876 5         10 $form->{audience} = $params{audience};
877 5         8 $form->{grant_type} = 'urn:ietf:params:oauth:grant-type:token-exchange';
878 5         9 $form->{subject_token} = $params{token};
879 5         10 $form->{subject_token_type} = 'urn:ietf:params:oauth:token-type:access_token';
880              
881 5 100 100     21 if (my $scope = ($params{scope} || $self->get_scope_for_audience($params{audience}))) {
882 2         5 $form->{scope} = $scope;
883             }
884              
885 5         20 $self->log_msg(debug => 'OIDC: calling provider to exchange token');
886              
887 5         413 my $res = $self->user_agent->post($token_url, $headers, form => $form)->result;
888              
889 5         692 return $self->token_response_parser->parse($res);
890             }
891              
892              
893             =head2 build_api_useragent( %args )
894              
895             my $ua = $client->build_api_useragent();
896              
897             Invokes the L</"get_token( %args )"> method and builds a web client (L<Mojo::UserAgent>
898             object) that will have the given access token in the authorization header for each request.
899              
900             This method can be useful if the client is configured for a password grant
901             or a client credentials grant and you simply want to build a web client to call
902             an API protected by OAuth2.
903              
904             =cut
905              
906             sub build_api_useragent {
907 2     2 1 63 my $self = shift;
908 2         12 my (%params) = validated_hash(
909             \@_,
910             token => { isa => 'Str', optional => 1 }, # DEPRECATED!
911             token_type => { isa => 'Maybe[Str]', optional => 1 }, # DEPRECATED!
912             );
913              
914 2 100       607 if (my $token_value = $params{token}) {
915 1         28 warnings::warnif('deprecated',
916             q{$oidc_client->build_api_useragent(token => $token_value) is deprecated, use } .
917             q{OIDC::Client::ApiUserAgentBuilder::build_api_useragent_from_token_value($token_value) function instead});
918 1         636 my $token_type = $params{token_type};
919 1         8 return build_api_useragent_from_token_value($token_value, $token_type);
920             }
921              
922 1         4 my $token_response = $self->get_token();
923 1         5 return build_api_useragent_from_token_response($token_response);
924             }
925              
926              
927             =head2 logout_url( %args )
928              
929             my $logout_url = $client->logout_url(%args);
930              
931             URL allowing the end-user to logout.
932             Returns a scalar or a L<Mojo::URL> object which contain the logout URL.
933              
934             The optional parameters are:
935              
936             =over 2
937              
938             =item id_token
939              
940             Content of the end-user's ID token.
941              
942             =item state
943              
944             String to add to the logout request that will be included when redirecting
945             to the C<post_logout_redirect_uri>.
946              
947             =item post_logout_redirect_uri
948              
949             Redirect URL value that indicates where to redirect the user after logout.
950             Can also be specified in the C<post_logout_redirect_uri> configuration entry.
951              
952             =item extra_params
953              
954             Hashref which can be used to send extra query parameters.
955              
956             =item want_mojo_url
957              
958             Defines whether you want this method to return a L<Mojo::URL> object
959             instead of a scalar. False by default.
960              
961             =back
962              
963             =cut
964              
965             sub logout_url {
966 4     4 1 5206 my $self = shift;
967 4         49 my (%params) = validated_hash(
968             \@_,
969             id_token => { isa => 'Str', optional => 1 },
970             state => { isa => 'Str', optional => 1 },
971             post_logout_redirect_uri => { isa => 'Str', optional => 1 },
972             extra_params => { isa => 'HashRef', optional => 1 },
973             want_mojo_url => { isa => 'Bool', default => 0 },
974             );
975              
976             my $end_session_url = $self->provider_metadata->{end_session_url}
977 4 100       1949 or croak("OIDC: end_session_url not found in provider metadata");
978              
979 3         84 my %args = (
980             client_id => $self->id,
981             );
982              
983 3 100       11 if (my $id_token = $params{id_token}) {
984 2         7 $args{id_token_hint} = $id_token;
985             }
986              
987 3 100       8 if (defined $params{state}) {
988 1         189 $args{state} = $params{state};
989             }
990              
991 3 100 100     82 if (my $redirect_uri = $params{post_logout_redirect_uri} || $self->post_logout_redirect_uri) {
992 2         5 $args{post_logout_redirect_uri} = $redirect_uri;
993             }
994              
995 3 100 100     119 if (my $extra_params = $params{extra_params} || $self->logout_extra_params) {
996 2         7 foreach my $param_name (keys %$extra_params) {
997 2         7 $args{$param_name} = $extra_params->{$param_name};
998             }
999             }
1000              
1001 3         24 my $logout_url = Mojo::URL->new($end_session_url);
1002 3         360 $logout_url->query(%args);
1003              
1004 3 100       218 return $params{want_mojo_url} ? $logout_url : $logout_url->to_string;
1005             }
1006              
1007              
1008             =head2 get_claim_value( %args )
1009              
1010             my $claim_value = $client->get_claim_value(name => 'login', claims => $claims);
1011              
1012             Returns the value of a claim by its configured name.
1013              
1014             The hash parameters are:
1015              
1016             =over 2
1017              
1018             =item name
1019              
1020             Name of the claim configured in the C<claim_mapping> section.
1021              
1022             =item claims
1023              
1024             Hashref of the claims.
1025              
1026             =item optional
1027              
1028             Defines whether the wanted claim must exist in the claims.
1029              
1030             =back
1031              
1032             =cut
1033              
1034             sub get_claim_value {
1035 5     5 1 5555 my $self = shift;
1036 5         71 my (%params) = validated_hash(
1037             \@_,
1038             name => { isa => 'Str', optional => 0 },
1039             claims => { isa => 'HashRef', optional => 0 },
1040             optional => { isa => 'Bool', default => 0 },
1041             );
1042              
1043             my $claim_key = $self->claim_mapping->{$params{name}}
1044 5 100       3661 or croak("OIDC: no claim key in config for name '$params{name}'");
1045              
1046 4         18 my @path = split /\./, $claim_key;
1047              
1048 4         22 return reach_data($params{claims}, \@path, $params{optional});
1049             }
1050              
1051              
1052             sub _decode_token {
1053 28     28   69 my ($self, $token, $has_already_update_keys) = @_;
1054              
1055             return try {
1056 28     28   2599 Crypt::JWT::decode_jwt(%{ $self->jwt_decoding_options },
  28         1277  
1057             token => $token,
1058             kid_keys => $self->kid_keys,
1059             decode_payload => 1,
1060             decode_header => 1);
1061             }
1062             catch {
1063 4     4   2222 my $e = $_;
1064 4 100 100     50 if ($e =~ /kid_keys/i && !$has_already_update_keys) {
1065 2         20 $self->log_msg(info => "OIDC: couldn't decode the token. Let's retry after updating the keys : $e");
1066 2         335 $self->_clear_kid_keys();
1067 2         13 return $self->_decode_token($token, 1);
1068             }
1069             else {
1070 2         47 OIDC::Client::Error::TokenValidation->throw("$e");
1071             }
1072 28         286 };
1073             }
1074              
1075              
1076             =head2 generate_uuid_string()
1077              
1078             Generates and returns a UUID string.
1079              
1080             =cut
1081              
1082             sub generate_uuid_string {
1083 7     7 1 13 my $self = shift;
1084 7         227 return $self->uuid_generator->create_str();
1085             }
1086              
1087              
1088             __PACKAGE__->meta->make_immutable;
1089              
1090              
1091             1;
1092              
1093             =head1 CONFIGURATION
1094              
1095             To use this module directly via a batch or script, here is the section to add
1096             to your configuration file:
1097              
1098             oidc_client:
1099             provider: provider_name
1100             id: my-app-id
1101             secret: xxxxxxxxx
1102             audience: other_app_name
1103             well_known_url: https://yourprovider.com/oauth2/.well-known/openid-configuration
1104             scope: roles
1105             token_endpoint_grant_type: password
1106             username: TECHXXXX
1107             password: xxxxxxxx
1108              
1109             This is an example, see the detailed possibilities in L<OIDC::Client::Config>.
1110              
1111             =head1 SAMPLES
1112              
1113             Here are some samples by category. Although you will have to adapt them to your needs,
1114             they should be a good starting point.
1115              
1116             =head2 API call
1117              
1118             To make an API call to another application :
1119              
1120             my $oidc_client = OIDC::Client->new(
1121             log => $self->log,
1122             config => $self->config->{oidc_client},
1123             );
1124              
1125             # Retrieving a web client (Mojo::UserAgent object)
1126             my $ua = $oidc_client->build_api_useragent();
1127              
1128             # Usual call to the API
1129             my $res = $ua->get($url)->result;
1130              
1131             Here, there is no token exchange because the audience has been configured
1132             to get the access token intended for the other application.
1133              
1134             =head2 API call with token expiration management
1135              
1136             If you need to manage token expiration because your script runs longer than
1137             the token lifetime, here is an example using Moose attributes :
1138              
1139             has 'oidc_client' => (
1140             is => 'ro',
1141             isa => 'OIDC::Client',
1142             lazy => 1,
1143             builder => '_build_oidc_client',
1144             );
1145              
1146             has 'access_token' => (
1147             is => 'ro',
1148             isa => 'OIDC::Client::AccessToken',
1149             lazy => 1,
1150             builder => '_build_access_token',
1151             clearer => '_clear_access_token',
1152             );
1153              
1154             has 'api_useragent' => (
1155             is => 'ro',
1156             isa => 'Mojo::UserAgent',
1157             lazy => 1,
1158             builder => '_build_api_useragent',
1159             clearer => '_clear_api_useragent',
1160             );
1161              
1162             sub _build_oidc_client {
1163             my $self = shift;
1164              
1165             return OIDC::Client->new(
1166             log => $self->log,
1167             config => $self->config->{oidc_client},
1168             );
1169             }
1170              
1171             sub _build_access_token {
1172             my $self = shift;
1173              
1174             my $token_response = $self->oidc_client->get_token();
1175              
1176             return build_access_token_from_token_response($token_response);
1177             }
1178              
1179             sub _build_api_useragent {
1180             my $self = shift;
1181              
1182             return build_api_useragent_from_access_token($self->access_token);
1183             }
1184              
1185             sub do_stuff {
1186             my $self = shift;
1187              
1188             if ($self->access_token->has_expired($self->oidc_client->expiration_leeway)) {
1189             $self->_clear_access_token();
1190             $self->_clear_api_useragent();
1191             }
1192              
1193             my $res = $self->api_useragent->get($self->config->{api_url})->result;
1194             ...
1195             }
1196              
1197             =head1 AUTHOR
1198              
1199             Sébastien Mourlhou
1200              
1201             =head1 COPYRIGHT AND LICENSE
1202              
1203             Copyright (C) Sébastien Mourlhou
1204              
1205             This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.
1206              
1207             =head1 SEE ALSO
1208              
1209             =over 2
1210              
1211             =item * L<Mojolicious::Plugin::OIDC>
1212              
1213             =item * L<Catalyst::Plugin::OIDC>
1214              
1215             =item * L<Dancer2::Plugin::OIDC>
1216              
1217             =back
1218              
1219             =cut