NAME

WWW::Authentik::OIDC - OpenID Connect against one authentik application

VERSION

version 0.001

SYNOPSIS

my $oidc = WWW::Authentik->new( base_url => $url, application => 'my-app' )->oidc;

my $claims = $oidc->verify_token( $jwt, type => 'access' );
my $tokens = $oidc->client_credentials_token( client_id => $id, client_secret => $secret, scope => 'openid' );

# give the client id once and every verify_token checks the audience
my $safe = WWW::Authentik->new( base_url => $url, application => 'my-app', client_id => $id )->oidc;

DESCRIPTION

The OpenID Connect side of one application: discovery, the signing keys, token verification, and the token endpoint in the grant types authentik offers.

Endpoints come from the application's discovery document, fetched once and kept. Only discovery, the keys and the end-session endpoint live under the application slug; the token, userinfo, introspection, revocation and device endpoints are shared by the whole instance, and the discovery document names them.

An error from the token endpoint is a WWW::Authentik::Error::API whose oauth_error carries the OAuth code, so a device-flow poll can tell authorization_pending from a real failure.

There is no direct grant for a normal user: authentik's grant_type=password is the same door as client_credentials with a username, and it only opens for a service account with an app-password token. A login as a person goes through the flow executor, which is not part of this distribution.

application_url

Required. <base_url>/application/o/<slug>, without a trailing slash.

ua

Required. The LWP::UserAgent to use.

client_id

The client id of this application's provider. When it is set, "verify_token" checks it as the audience unless the caller names another one. Setting it is the simple way to be safe on an instance whose providers issue tokens under a shared issuer; see "verify_token".

algorithms

Signature algorithms "verify_token" accepts. Never none, never HMAC. authentik signs with RS256.

jwks_min_age

Seconds that have to pass before "verify_token" fetches the keys again for a token signed with an unknown key. Default 60, so that a stream of forged tokens does not become a stream of requests to authentik.

now

Coderef returning the current epoch. For tests.

discovery

The discovery document, fetched on first use and kept.

endpoint

my $url = $oidc->endpoint('device_authorization_endpoint');

A URL from the discovery document. Throws when it is missing.

issuer

print $oidc->issuer;   # https://id.example.org/application/o/my-app/

What authentik puts into iss and what "verify_token" checks against. With the provider set to issuer_mode: global this is the bare instance URL, not the application address.

authorization_endpoint

token_endpoint

userinfo_endpoint

introspection_endpoint

revocation_endpoint

end_session_endpoint

device_endpoint

jwks_uri

The endpoints out of the discovery document.

jwks

my $keys = $oidc->jwks;
my $keys = $oidc->jwks( force_refresh => 1 );

The application's public signing keys, kept after the first fetch.

issuer_names_the_application

True when the issuer out of the discovery document is this application's own address, which is what issuer_mode: per_provider gives. False under issuer_mode: global, where every provider of the instance issues tokens under the bare instance URL.

verify_token

my $claims = $oidc->verify_token( $jwt );
my $claims = $oidc->verify_token( $jwt, audience => $client_id, type => 'access' );

Checks signature, issuer and expiry, and the audience. Returns the claims, or throws a WWW::Authentik::Error::Validation saying why the token was rejected. When the signing key is unknown the keys are fetched again, at most once per "jwks_min_age".

The audience is what separates two applications of one authentik. Every provider of an instance signs with the same key, so the issuer is the only other thing that could tell them apart — and with issuer_mode: global the issuer is the bare instance URL, the same for all of them. Verifying without an audience on such an instance would accept any token it ever issued, so this method refuses to do it: give audience, set "client_id" on the client so it is used by itself, or pass any_audience => 1 to say that accepting every application of this authentik is what you meant. Under the default issuer_mode: per_provider the issuer already names the application, and an audience is then optional.

type is a heuristic, and a weak one. authentik signs ID tokens and access tokens the same way and puts no typ into the JOSE header: both are RS256 JWTs with "typ": "JWT". The only reliable difference observed is that an access token carries a scope claim and an ID token does not. So type => 'access' rejects a token without scope, type => 'id' rejects one with it, and neither is a cryptographic distinction. Without type the kind of token is not checked at all. Where it matters that a token was meant for a particular API, check audience.

userinfo

my $info = $oidc->userinfo($access_token);

The claims authentik gives out for this token. A token it does not accept is a WWW::Authentik::Error::API with status 401 and oauth_error invalid_token, read out of the WWW-Authenticate header, because the body is empty.

introspect

my $state = $oidc->introspect( $token, client_id => $id, client_secret => $secret );

Needs a confidential client. An access token and a refresh token both come back with active => true and their claims; anything else, including a token the client is not allowed to see, is { active => false } with status 200.

revoke

$oidc->revoke( $access_token, client_id => $id, client_secret => $secret );

Ends a token. authentik answers 200 for a token it does not know as well, so a true return means the call went through, not that something was revoked. A wrong client secret is a 401 invalid_client.

client_credentials_token

my $tokens = $oidc->client_credentials_token( client_id => $id, client_secret => $secret, scope => 'openid' );
my $tokens = $oidc->client_credentials_token( client_id => $id, username => 'svc', password => $app_password );

The client credentials grant. With a client secret the token belongs to the service account authentik creates for the provider (ak-<provider>-client_credentials). With username and password it belongs to that service account, where the password is the app-password token WWW::Authentik::API->create_service_account handed out.

refresh_token

my $tokens = $oidc->refresh_token( $refresh, client_id => $id, client_secret => $secret );

authentik rotates refresh tokens: the answer carries a new one and the old one is dead. Asking for fewer scopes than were granted is an invalid_scope. The claims, including amr and auth_time, carry over unchanged.

exchange_authorization_code

my $tokens = $oidc->exchange_authorization_code( code => $code, redirect_uri => $uri,
  client_id => $id, client_secret => $secret );

A code can be exchanged once; the second try is an invalid_grant.

device_authorization

my $start = $oidc->device_authorization( client_id => $id, scope => 'openid' );

Starts a device flow: device_code, user_code, verification_uri, verification_uri_complete, expires_in and interval. The brand needs a flow_device_code for a person to be able to approve it.

device_token

my $tokens = eval { $oidc->device_token( device_code => $start->{device_code},
  client_id => $id, client_secret => $secret ) };
# $@->oauth_error eq 'authorization_pending' while nobody has approved

One poll of the device flow; the loop is the caller's. authentik answers authorization_pending however fast the polling is and never slow_down, and an expired or already used code is an invalid_grant.

authorization_url

my $url = $oidc->authorization_url( client_id => $id, redirect_uri => $uri,
  scope => 'openid email', state => $state, nonce => $nonce );

The address a browser is sent to. This distribution does not follow it: without a session authentik answers 302 into its own flow interface, and driving that is a browser's job, or the flow executor's.

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-www-authentik/issues.

IRC

Join #kubernetes on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.