package OIDC::Client;
use 5.020;
use utf8;
use Moose;
use Moose::Util::TypeConstraints;
use MooseX::Params::Validate;
use namespace::autoclean;

use Carp qw(croak);
use List::Util qw(first);
use Try::Tiny;
use Crypt::JWT ();
use Mojo::URL;
use OIDC::Client::Error::TokenValidation;
use OIDC::Client::ApiUserAgentBuilder qw(build_api_useragent_from_token_response
                                         build_api_useragent_from_token_value);

our $VERSION = '1.05'; # VERSION: generated by Dist::Zilla::Plugin::OurPkgVersion

=encoding utf8

=head1 NAME

OIDC::Client - OpenID Connect Client

=head1 SYNOPSIS

  my $client = OIDC::Client->new(
    provider          => 'my_provider',
    id                => 'my_client_id',
    secret            => 'my_client_secret',
    provider_metadata => \%provider_metadata,
    log               => $app->log,
  );

  # or...

  my $client = OIDC::Client->new(
    config => $config_provider,
    log    => $app->log,
  );

  my $authorize_url  = $client->auth_url();
  my $token_response = $client->get_token(code => $code);
  my $claims         = $client->verify_jwt_token(token => $token);
  my $claims         = $client->introspect_token(token => $token);
  my $userinfo       = $client->get_userinfo(access_token => $token);
  my $token_response = $client->exchange_token(token => $token, audience => $audience);
  my $ua             = $client->build_api_useragent();
  my $logout_url     = $client->logout_url();

=head1 DESCRIPTION

Client module for OpenID Connect and OAuth 2.0 protocols.

Use this module directly from a batch or a simple script. For use from within
an application, you should instead use one of these framework plugins, which
all use this distribution :

=over 2

=item * L<Mojolicious::Plugin::OIDC>

=item * L<Catalyst::Plugin::OIDC>

=item * L<Dancer2::Plugin::OIDC>

=back

=cut

enum 'StoreMode'             => [qw/session stash cache/];
enum 'ResponseMode'          => [qw/query form_post/];
enum 'GrantType'             => [qw/authorization_code client_credentials password refresh_token/];
enum 'ClientAuthMethod'      => [qw/client_secret_basic client_secret_post client_secret_jwt private_key_jwt none/];
enum 'TokenValidationMethod' => [qw/jwt introspection/];

with 'OIDC::Client::Role::LoggerWrapper';
with 'OIDC::Client::Role::AttributesManager';
with 'OIDC::Client::Role::ConfigurationChecker';
with 'OIDC::Client::Role::ClaimsValidator';
with 'OIDC::Client::Role::ClientAuthenticationHelper';

=head1 METHODS

=head2 BUILD

Called after the object is created. Makes some basic checks and forces
the retrieval of provider metadata and kid keys.

=cut

sub BUILD {
  my $self = shift;

  $self->_check_configuration();
  $self->_check_audiences_configuration();
  $self->_check_cache_configuration();

  $self->provider;
  $self->id;

  $self->provider_metadata;
  $self->kid_keys if $self->provider_metadata->{jwks_url};
}


=head2 auth_url( %args )

  my $authorize_url = $client->auth_url(%args);

Returns a scalar or a L<Mojo::URL> object containing the initial authorization URL.
This is the URL to use to initiate an authorization code flow.

The optional parameters are:

=over 2

=item response_mode

Defines how tokens are sent by the provider.

Can take one of these values:

=over 2

=item query

Tokens are sent in query parameters.

=item form_post

Tokens are sent in a POST form.

=back

=item redirect_uri

Redirection URI to which the response will be sent.
Can also be specified in the C<signin_redirect_uri> configuration entry.

=item state

String which is sent during the request to the identity provider
and sent back from the IDP along with the Code.

=item nonce

String which is sent during the request to the identity provider
and sent back from the IDP in the returned ID Token.

=item scope

Specifies the desired scope of the requested token.
Must be a string with space separators.
Can also be specified in the C<scope> configuration entry.

=item audience

Specifies the audience/resource that the access token is intended for.
Can also be specified in the C<audience> configuration entry.

=item extra_params

Hashref which can be used to send extra query parameters.
Can also be specified in the C<authorize_endpoint_extra_params> configuration entry.

=item want_mojo_url

Defines whether you want this method to return a L<Mojo::URL> object
instead of a scalar. False by default.

=back

=cut

sub auth_url {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    response_mode => { isa => 'ResponseMode', optional => 1 },
    redirect_uri  => { isa => 'Str', optional => 1 },
    state         => { isa => 'Str', optional => 1 },
    nonce         => { isa => 'Str', optional => 1 },
    scope         => { isa => 'Str', optional => 1 },
    audience      => { isa => 'Str', optional => 1 },
    extra_params  => { isa => 'HashRef', optional => 1 },
    want_mojo_url => { isa => 'Bool', default => 0 },
  );

  my $authorize_url = $self->provider_metadata->{authorize_url}
    or croak("OIDC: authorize url not found in provider metadata");

  my %args = (
    response_type => 'code',
    client_id     => $self->id,
  );

  if (my $response_mode = $params{response_mode} || $self->authorize_endpoint_response_mode) {
    $args{response_mode} = $response_mode;
  }

  if (my $redirect_uri = $params{redirect_uri} || $self->signin_redirect_uri) {
    $args{redirect_uri} = $redirect_uri;
  }

  foreach my $param_name (qw/state nonce/) {
    if (defined $params{$param_name}) {
      $args{$param_name} = $params{$param_name};
    }
  }

  if (my $scope = $params{scope} || $self->scope) {
    $args{scope} = $scope;
  }

  if (my $audience = $params{audience} || ($self->audience ne $self->id ? $self->audience : undef)) {
    $args{audience} = $audience;
  }

  if (my $extra_params = $params{extra_params} || $self->authorize_endpoint_extra_params) {
    foreach my $param_name (keys %$extra_params) {
      $args{$param_name} = $extra_params->{$param_name};
    }
  }

  my $auth_url = Mojo::URL->new($authorize_url);
  $auth_url->query(%args);

  return $params{want_mojo_url} ? $auth_url : $auth_url->to_string;
}


=head2 get_token( %args )

  my $token_response = $client->get_token(
    code         => $code,
    redirect_uri => q{http://yourapp/oidc/callback},
  );

Fetch token(s) from an OAuth2/OIDC provider and returns an
L<OIDC::Client::TokenResponse> object.

This method doesn't perform any verification on the ID token.
Call the L</"verify_jwt_token( %args )"> method to do so.

The optional parameters are:

=over 2

=item grant_type

Specifies how the client wants to interact with the identity provider.
Accepted here : C<authorization_code>, C<client_credentials>, C<password>
or C<refresh_token>.
Can also be specified in the C<token_endpoint_grant_type> configuration entry.
Default to C<authorization_code>.

=item auth_method

Specifies how the client authenticates with the identity provider.

Supported client authentication methods:

=over 2

=item *

client_secret_basic (default)

=item *

client_secret_post

=item *

client_secret_jwt

=item *

private_key_jwt

=item *

none

=back

Can also be specified in the C<token_endpoint_auth_method> configuration entry
or the global C<client_auth_method> configuration entry.
Default to C<client_secret_basic>.

=item code

Authorization-code that is issued beforehand by the identity provider.
Used only for the C<authorization_code> grant-type.

=item redirect_uri

Redirection URI to which the response will be sent.
Can also be specified in the C<signin_redirect_uri> configuration entry.
Used only for the C<authorization_code> grant-type.

=item username / password

User credentials for authorization
Can also be specified in the C<username> and C<password> configuration entries.
Used only the for C<password> grant-type.

=item audience

Specifies the Relaying Party for which the token is intended.
Can also be specified in the C<audience> configuration entry.
Not used for the C<refresh_token> grant-type.

=item scope

Specifies the desired scope of the requested token.
Must be a string with space separators.
Can also be specified in the C<scope> configuration entry.
Not used for the C<authorization_code> nor the C<refresh_token> grant-type.

=item refresh_token

Token that can be used to renew the associated access token before it expires.
Used only for the C<refresh_token> grant-type.

=item refresh_scope

Specifies the desired scope of the requested renewed token.
Must be a string with space separators.
Used only for the C<refresh_token> grant-type.

=back

=cut

sub get_token {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    grant_type    => { isa => 'GrantType', optional => 1 },
    auth_method   => { isa => 'ClientAuthMethod', optional => 1 },
    code          => { isa => 'Str', optional => 1 },
    redirect_uri  => { isa => 'Str', optional => 1 },
    username      => { isa => 'Str', optional => 1 },
    password      => { isa => 'Str', optional => 1 },
    audience      => { isa => 'Str', optional => 1 },
    scope         => { isa => 'Str', optional => 1 },
    refresh_token => { isa => 'Str', optional => 1 },
    refresh_scope => { isa => 'Str', optional => 1 },
  );

  my $grant_type = $params{grant_type} || $self->token_endpoint_grant_type;

  my $token_url = $self->provider_metadata->{token_url}
    or croak("OIDC: token url not found in provider metadata");

  my $auth_method = $params{auth_method} || $self->token_endpoint_auth_method;
  my ($headers, $form) = $self->_build_client_auth_arguments($auth_method, $token_url);

  $form->{grant_type} = $grant_type;

  if ($grant_type eq 'authorization_code') {
    $form->{code} = $params{code}
      or croak("OIDC: code is missing");

    if (my $redirect_uri = $params{redirect_uri} || $self->signin_redirect_uri) {
      $form->{redirect_uri} = $redirect_uri;
    }
  }
  elsif ($grant_type eq 'password') {
    foreach my $required_field (qw/username password/) {
      $form->{$required_field} = ($params{$required_field} || $self->$required_field)
        or croak("OIDC: $required_field is missing");
    }
  }
  elsif ($grant_type eq 'refresh_token') {
    $form->{refresh_token} = $params{refresh_token}
      or croak("OIDC: refresh_token is missing");

    # Only the 'refresh_scope' method parameter should be considered here. The configuration
    # parameter is used by Plugin.pm which knows for which audience the token is renewed
    if (my $scope = $params{refresh_scope}) {
      $form->{scope} = $scope;
    }
  }

  unless ($grant_type =~ /^(authorization_code|refresh_token)$/) {
    if (my $scope = ($params{scope} || $self->scope)) {
      $form->{scope} = $scope;
    }
  }

  unless ($grant_type eq 'refresh_token') {
    if (my $audience = $params{audience} || ($self->audience ne $self->id ? $self->audience : undef)) {
      $form->{audience} = $audience;
    }
  }

  $self->log_msg(debug => 'OIDC: calling provider to get token');

  my $res = $self->user_agent->post($token_url, $headers, form => $form)->result;

  return $self->token_response_parser->parse($res);
}


=head2 verify_jwt_token( %args )

  my $claims = $client->verify_jwt_token(
    token             => $token,
    expected_audience => $audience,
    expected_nonce    => $nonce,
  );

Checks the structure, claims and signature of the JWT token.
Throws an L<OIDC::Client::Error::TokenValidation> exception if an error occurs.
Otherwise, returns the claims (hashref).

This method automatically manages a JWK key rotation. If a JWK key error
is detected during token verification, the JWK keys in memory are refreshed
by retrieving them again from the JWKS URL. The token is checked again, and if
an error occurs, an L<OIDC::Client::Error::TokenValidation> exception is thrown.

The following claims are validated :

=over 2

=item "exp" (Expiration Time) claim

Must be present and valid (in the future).

=item "iat" (Issued At) claim

Must be present and valid (not in the future).

=item "nbf" (Not Before) claim

Must be valid (not in the future) if present.

=item "iss" (Issuer) claim

Must be the issuer recorded in the provider metadata.

=item "aud" (Audience) claim

Must be the expected audience (see parameters beelow).

=item "sub" (Subject) claim

Must be the expected subject defined in the parameters (see beelow).

=back

The L<Crypt::JWT::decode_jwt()|https://metacpan.org/pod/Crypt::JWT#decode_jwt>
function is used to validate and decode a JWT token. Remember that you can change
the options transmitted to this function (see L<OIDC::Client::Config>).

The parameters are:

=over 2

=item token

The JWT token to validate.
Required.

=item expected_audience

If the token is not intended for the expected audience, an exception is thrown.
Default to the C<audience> configuration entry or otherwise the client id.

=item expected_authorized_party

If the C<azp> claim value is not the expected authorized_party, an exception is thrown
unless the JWT token has no C<azp> claim and the C<no_authorized_party_accepted>
parameter is true.
Optional.

=item no_authorized_party_accepted

When the C<expected_authorized_party> parameter is defined, prevents an exception
from being thrown if the JWT token does not contain an C<azp> claim.
Default to false.

=item expected_subject

If the C<sub> claim value is not the expected subject, an exception is thrown.
Optional.

=item expected_nonce

If the C<nonce> claim value is not the expected nonce, an exception is thrown
unless the JWT token has no C<nonce> claim and the C<no_nonce_accepted> parameter is true.
Optional.

=item no_nonce_accepted

When the C<expected_nonce> parameter is defined, prevents an exception from being
thrown if the JWT token does not contain a C<nonce> claim.
Default to false.

=item want_header

  my ($header, $claims) = $client->verify_jwt_token(
    token       => $token,
    want_header => 1,
  );

Defines whether you want the decoded header to be returned by this method.
False by default.

=back

=cut

sub verify_jwt_token {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    token                        => { isa => 'Str', optional => 0 },
    expected_audience            => { isa => 'Str', optional => 1 },
    expected_authorized_party    => { isa => 'Maybe[Str]', optional => 1 },
    no_authorized_party_accepted => { isa => 'Bool', default => 0 },
    expected_subject             => { isa => 'Str', optional => 1 },
    expected_nonce               => { isa => 'Str', optional => 1 },
    no_nonce_accepted            => { isa => 'Bool', default => 0 },
    max_token_age                => { isa => 'Int', optional => 1 },
    want_header                  => { isa => 'Bool', default => 0 },
  );

  # checks the signature and the timestamps
  my ($header, $claims) = $self->_decode_token($params{token});

  # checks the issuer
  $self->_validate_issuer($claims->{iss});

  # checks the audience
  $self->_validate_audience($claims->{aud}, $params{expected_audience});

  # checks the authorized party
  if (exists $params{expected_authorized_party}) {
    unless (!exists $claims->{azp} && $params{no_authorized_party_accepted}) {
      $self->_validate_authorized_party($claims->{azp}, $params{expected_authorized_party});
    }
  }

  # checks the subject
  if (my $expected_subject = $params{expected_subject}) {
    $self->_validate_subject($claims->{sub}, $expected_subject);
  }

  # checks the nonce
  if (my $expected_nonce = $params{expected_nonce}) {
    unless (!exists $claims->{nonce} && $params{no_nonce_accepted}) {
      $self->_validate_nonce($claims->{nonce}, $expected_nonce);
    }
  }

  # checks that the token has not been issued too far away from the current time
  if (my $max_token_age = $params{max_token_age}) {
    $self->_validate_age($claims->{iat}, $max_token_age);
  }

  return $params{want_header} ? ($header, $claims) : $claims;
}

=head2 verify_token( %args )

This method is DEPRECATED! Instead, use L</"verify_jwt_token( %args )">.

=cut

sub verify_token {
  my ($self, %params) = @_;
  warnings::warnif('deprecated',
                   'OIDC::Client::verify_token() is deprecated in favor of OIDC::Client::verify_jwt_token()');
  return $self->verify_jwt_token(%params);
}


=head2 introspect_token( %args )

  my $claims = $client->introspect_token(
    token           => $token,
    token_type_hint => 'access_token',
  );

Allows a Resource Server to validate a token and obtain its metadata by calling the provider's
introspection endpoint. Typically used when the access token is opaque (not a JWT).

Throws an L<OIDC::Client::Error::Provider> exception if an error is returned by the provider
or an L<OIDC::Client::Error::TokenValidation> exception if a validation error occurs.
Otherwise, returns the claims.

The parameters are:

=over 2

=item token

The token to validate.
Required.

=item token_type_hint

Hint about the type of the token submitted for introspection.

=item auth_method

Specifies how the client authenticates with the identity provider.

Supported client authentication methods:

=over 2

=item *

client_secret_basic (default)

=item *

client_secret_post

=item *

client_secret_jwt

=item *

private_key_jwt

=item *

none

=back

Can also be specified in the C<introspection_endpoint_auth_method> configuration entry
or the global C<client_auth_method> configuration entry.
Default to C<client_secret_basic>.

=item expected_audience

If the C<aud> claim is present in the provider response of the introspection endpoint,
its value must match the expected audience, otherwise an exception is thrown.
Default to the C<audience> configuration entry or the client id.

=back

=cut

sub introspect_token {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    token             => { isa => 'Str', optional => 0 },
    token_type_hint   => { isa => enum([qw/access_token refresh_token/]), optional => 1 },
    auth_method       => { isa => 'ClientAuthMethod', optional => 1 },
    expected_audience => { isa => 'Str', optional => 1 },
  );

  my $introspection_url = $self->provider_metadata->{introspection_url}
    or croak("OIDC: introspection_url not found in provider metadata");

  my $auth_method = $params{auth_method} || $self->introspection_endpoint_auth_method;
  my ($headers, $form) = $self->_build_client_auth_arguments($auth_method, $introspection_url);

  $form->{token} = $params{token};

  if (my $token_type_hint = $params{token_type_hint}) {
    $form->{token_type_hint} = $token_type_hint;
  }

  $self->log_msg(debug => 'OIDC: calling provider to introspect token');
  my $res = $self->user_agent->post($introspection_url, $headers, form => $form)->result;

  my $claims = $self->response_parser->parse($res);

  $claims->{active}
    or OIDC::Client::Error::TokenValidation->throw("OIDC: inactive token");

  if (exists $claims->{iss}) {
    $self->_validate_issuer($claims->{iss});
  }

  if (exists $claims->{aud}) {
    $self->_validate_audience($claims->{aud}, $params{expected_audience});
  }

  return $claims;
}


=head2 get_userinfo( %args )

  my $userinfo = $client->get_userinfo(
    access_token => $stored_token->token,
    token_type   => $stored_token->token_type,
  );

Get and returns the user information from an OAuth2/OIDC provider.

The parameters are:

=over 2

=item access_token

Content of the valid access token obtained through OIDC authentication.

=item token_type

Optional, default to C<Bearer>.

=back

=cut

sub get_userinfo {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    access_token => { isa => 'Str', optional => 0 },
    token_type   => { isa => 'Maybe[Str]', optional => 1 },
  );

  my $userinfo_url = $self->provider_metadata->{userinfo_url}
    or croak("OIDC: userinfo_url not found in provider metadata");

  my $token_type = $params{token_type} || $self->default_token_type;

  my $authorization = "$token_type $params{access_token}";

  $self->log_msg(debug => 'OIDC: calling provider to fetch userinfo');

  my $res = $self->user_agent->get($userinfo_url, { Authorization => $authorization })
                             ->result;

  return $self->response_parser->parse($res);
}


=head2 get_audience_for_alias( $audience_alias )

  my $audience = $client->get_audience_for_alias($audience_alias);

Returns the audience for an alias that has been configured in the configuration
entry C<audience_alias>/C<$audience_alias>/C<audience>.

=cut

sub get_audience_for_alias {
  my $self = shift;
  my ($alias) = pos_validated_list(\@_, { isa => 'Str', optional => 0 });

  my $audience_alias = $self->audience_alias
    or return;

  my $audience_infos = $audience_alias->{$alias}
    or return;

  return $audience_infos->{audience};
}


=head2 get_scope_for_audience( $audience )

  my $scope = $client->get_scope_for_audience($audience);

Returns the scope for an audience that has been configured in the configuration
entry C<audience_alias>/C<$audience_alias>/C<scope>.

=cut

sub get_scope_for_audience {
  my $self = shift;
  my ($audience) = pos_validated_list(\@_, { isa => 'Str', optional => 0 });

  my $audience_alias = $self->audience_alias
    or return;

  my $audience_infos = first { $_->{audience} eq $audience  } values %$audience_alias
    or return;

  return $audience_infos->{scope};
}


=head2 exchange_token( %args )

  my $exchanged_token_response = $client->exchange_token(
    token    => $token,
    audience => $audience,
  );

Exchanges an access token, obtained through OIDC authentication, for another access
token that is accepted by a different OIDC application.

Returns a L<OIDC::Client::TokenResponse> object.

The parameters are:

=over 2

=item token

Content of the valid access token obtained through OIDC authentication.

=item audience

Audience of the target application.

=item scope

Specifies the desired scope of the requested token.
Must be a string with space separators.
Optional.

=item auth_method

Specifies how the client authenticates with the identity provider.

Supported client authentication methods:

=over 2

=item *

client_secret_basic (default)

=item *

client_secret_post

=item *

client_secret_jwt

=item *

private_key_jwt

=item *

none

=back

Can also be specified in the C<token_endpoint_auth_method> configuration entry
or the global C<client_auth_method> configuration entry.
Default to C<client_secret_basic>.

=back

=cut

sub exchange_token {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    token       => { isa => 'Str', optional => 0 },
    audience    => { isa => 'Str', optional => 0 },
    scope       => { isa => 'Str', optional => 1 },
    auth_method => { isa => 'ClientAuthMethod', optional => 1 },
  );

  my $token_url = $self->provider_metadata->{token_url}
    or croak("OIDC: token url not found in provider metadata");

  my $auth_method = $params{auth_method} || $self->token_endpoint_auth_method;
  my ($headers, $form) = $self->_build_client_auth_arguments($auth_method, $token_url);

  $form->{audience}           = $params{audience};
  $form->{grant_type}         = 'urn:ietf:params:oauth:grant-type:token-exchange';
  $form->{subject_token}      = $params{token};
  $form->{subject_token_type} = 'urn:ietf:params:oauth:token-type:access_token';

  if (my $scope = ($params{scope} || $self->get_scope_for_audience($params{audience}))) {
    $form->{scope} = $scope;
  }

  $self->log_msg(debug => 'OIDC: calling provider to exchange token');

  my $res = $self->user_agent->post($token_url, $headers, form => $form)->result;

  return $self->token_response_parser->parse($res);
}


=head2 build_api_useragent( %args )

  my $ua = $client->build_api_useragent();

Invokes the L</"get_token( %args )"> method and builds a web client (L<Mojo::UserAgent>
object) that will have the given access token in the authorization header for each request.

This method can be useful if the client is configured for a password grant
or a client credentials grant and you simply want to build a web client to call
an API protected by OAuth2.

=cut

sub build_api_useragent {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    token      => { isa => 'Str', optional => 1 },         # DEPRECATED!
    token_type => { isa => 'Maybe[Str]', optional => 1 },  # DEPRECATED!
  );

  if (my $token_value = $params{token}) {
    warnings::warnif('deprecated',
                     q{$oidc_client->build_api_useragent(token => $token_value) is deprecated, use } .
                     q{OIDC::Client::ApiUserAgentBuilder::build_api_useragent_from_token_value($token_value) function instead});
    my $token_type = $params{token_type};
    return build_api_useragent_from_token_value($token_value, $token_type);
  }

  my $token_response = $self->get_token();
  return build_api_useragent_from_token_response($token_response);
}


=head2 logout_url( %args )

  my $logout_url = $client->logout_url(%args);

URL allowing the end-user to logout.
Returns a scalar or a L<Mojo::URL> object which contain the logout URL.

The optional parameters are:

=over 2

=item id_token

Content of the end-user's ID token.

=item state

String to add to the logout request that will be included when redirecting
to the C<post_logout_redirect_uri>.

=item post_logout_redirect_uri

Redirect URL value that indicates where to redirect the user after logout.
Can also be specified in the C<post_logout_redirect_uri> configuration entry.

=item extra_params

Hashref which can be used to send extra query parameters.

=item want_mojo_url

Defines whether you want this method to return a L<Mojo::URL> object
instead of a scalar. False by default.

=back

=cut

sub logout_url {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    id_token                 => { isa => 'Str', optional => 1 },
    state                    => { isa => 'Str', optional => 1 },
    post_logout_redirect_uri => { isa => 'Str', optional => 1 },
    extra_params             => { isa => 'HashRef', optional => 1 },
    want_mojo_url            => { isa => 'Bool', default => 0 },
  );

  my $end_session_url = $self->provider_metadata->{end_session_url}
    or croak("OIDC: end_session_url not found in provider metadata");

  my %args = (
    client_id => $self->id,
  );

  if (my $id_token = $params{id_token}) {
    $args{id_token_hint} = $id_token;
  }

  if (defined $params{state}) {
    $args{state} = $params{state};
  }

  if (my $redirect_uri = $params{post_logout_redirect_uri} || $self->post_logout_redirect_uri) {
    $args{post_logout_redirect_uri} = $redirect_uri;
  }

  if (my $extra_params = $params{extra_params} || $self->logout_extra_params) {
    foreach my $param_name (keys %$extra_params) {
      $args{$param_name} = $extra_params->{$param_name};
    }
  }

  my $logout_url = Mojo::URL->new($end_session_url);
  $logout_url->query(%args);

  return $params{want_mojo_url} ? $logout_url : $logout_url->to_string;
}


=head2 get_claim_value( %args )

  my $claim_value = $client->get_claim_value(name => 'login', claims => $claims);

Returns the value of a claim by its configured name.

The hash parameters are:

=over 2

=item name

Name of the claim configured in the C<claim_mapping> section.

=item claims

Hashref of the claims.

=item optional

Defines whether the wanted claim must exist in the claims.

=back

=cut

sub get_claim_value {
  my $self = shift;
  my (%params) = validated_hash(
    \@_,
    name     => { isa => 'Str', optional => 0 },
    claims   => { isa => 'HashRef', optional => 0 },
    optional => { isa => 'Bool', default => 0 },
  );

  my $claim_key = $self->claim_mapping->{$params{name}}
    or croak("OIDC: no claim key in config for name '$params{name}'");

  unless ($params{optional}) {
    exists $params{claims}->{$claim_key}
      or croak("OIDC: the '$claim_key' claim is not present");
  }

  return $params{claims}->{$claim_key};
}


sub _decode_token {
  my ($self, $token, $has_already_update_keys) = @_;

  return try {
    Crypt::JWT::decode_jwt(%{ $self->jwt_decoding_options },
                           token    => $token,
                           kid_keys => $self->kid_keys,
                           decode_payload => 1,
                           decode_header  => 1);
  }
  catch {
    my $e = $_;
    if ($e =~ /kid_keys/i && !$has_already_update_keys) {
      $self->log_msg(info => "OIDC: couldn't decode the token. Let's retry after updating the keys : $e");
      $self->_clear_kid_keys();
      return $self->_decode_token($token, 1);
    }
    else {
      OIDC::Client::Error::TokenValidation->throw("$e");
    }
  };
}


=head2 generate_uuid_string()

Generates and returns a UUID string.

=cut

sub generate_uuid_string {
  my $self = shift;
  return $self->uuid_generator->create_str();
}


__PACKAGE__->meta->make_immutable;


1;

=head1 CONFIGURATION

To use this module directly via a batch or script, here is the section to add
to your configuration file:

  oidc_client:
    provider:                  provider_name
    id:                        my-app-id
    secret:                    xxxxxxxxx
    audience:                  other_app_name
    well_known_url:            https://yourprovider.com/oauth2/.well-known/openid-configuration
    scope:                     roles
    token_endpoint_grant_type: password
    username:                  TECHXXXX
    password:                  xxxxxxxx

This is an example, see the detailed possibilities in L<OIDC::Client::Config>.

=head1 SAMPLES

Here are some samples by category. Although you will have to adapt them to your needs,
they should be a good starting point.

=head2 API call

To make an API call to another application :

  my $oidc_client = OIDC::Client->new(
    log    => $self->log,
    config => $self->config->{oidc_client},
  );

  # Retrieving a web client (Mojo::UserAgent object)
  my $ua = $oidc_client->build_api_useragent();

  # Usual call to the API
  my $res = $ua->get($url)->result;

Here, there is no token exchange because the audience has been configured
to get the access token intended for the other application.

=head2 API call with token expiration management

If you need to manage token expiration because your script runs longer than
the token lifetime, here is an example using Moose attributes :

  has 'oidc_client' => (
    is      => 'ro',
    isa     => 'OIDC::Client',
    lazy    => 1,
    builder => '_build_oidc_client',
  );

  has 'access_token' => (
    is      => 'ro',
    isa     => 'OIDC::Client::AccessToken',
    lazy    => 1,
    builder => '_build_access_token',
    clearer => '_clear_access_token',
  );

  has 'api_useragent' => (
    is      => 'ro',
    isa     => 'Mojo::UserAgent',
    lazy    => 1,
    builder => '_build_api_useragent',
    clearer => '_clear_api_useragent',
  );

  sub _build_oidc_client {
    my $self = shift;

    return OIDC::Client->new(
      log    => $self->log,
      config => $self->config->{oidc_client},
    );
  }

  sub _build_access_token {
    my $self = shift;

    my $token_response = $self->oidc_client->get_token();

    return build_access_token_from_token_response($token_response);
  }

  sub _build_api_useragent {
    my $self = shift;

    return build_api_useragent_from_access_token($self->access_token);
  }

  sub do_stuff {
    my $self = shift;

    if ($self->access_token->has_expired($self->oidc_client->expiration_leeway)) {
      $self->_clear_access_token();
      $self->_clear_api_useragent();
    }

    my $res = $self->api_useragent->get($self->config->{api_url})->result;
    ...
  }

=head1 AUTHOR

Sébastien Mourlhou

=head1 COPYRIGHT AND LICENSE

Copyright (C) Sébastien Mourlhou

This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.

=head1 SEE ALSO

=over 2

=item * L<Mojolicious::Plugin::OIDC>

=item * L<Catalyst::Plugin::OIDC>

=item * L<Dancer2::Plugin::OIDC>

=back

=cut
