Catalyst-Plugin-OAuth2-AuthorizationServer

 view release on metacpan or  search on metacpan

lib/Catalyst/Plugin/OAuth2/AuthorizationServer/Server.pm  view on Meta::CPAN

        $self->_now + $self->refresh_ttl,
    );
    # The family was revoked while this rotation was in flight: a concurrent
    # replay was detected. Same generic error as any other dead token.
    $self->_grant_error('unknown or revoked refresh token') unless $created;
    return {
        access_token  => $access,
        token_type    => 'Bearer',
        expires_in    => $self->access_ttl,
        refresh_token => $refresh,
        ( defined $binding->{scope} ? ( scope => $binding->{scope} ) : () ),
    };
}

sub exchange_authorization_code ( $self, $params ) {
    my $code = $params->{code};
    $self->_grant_error('code is required')
        unless defined $code && length $code;

    my $binding = $self->store->consume_auth_code($code);
    $self->_grant_error('unknown or used authorization code') unless $binding;

    # When the request identifies its client, it must match the code's bound
    # client (RFC 6749 4.1.3 defence-in-depth on top of PKCE).
    if ( defined $params->{client_id} && length $params->{client_id} ) {
        $self->_grant_error('client_id mismatch')
            unless $params->{client_id} eq $binding->{client_id};
    }

    $self->_grant_error('redirect_uri mismatch')
        unless ( $params->{redirect_uri} // '' ) eq $binding->{redirect_uri};

    my $verifier = $params->{code_verifier};
    $self->_grant_error('code_verifier required')
        unless defined $verifier && length $verifier;
    # RFC 7636 4.1: 43-128 characters of the unreserved set.
    $self->_grant_error(
        'code_verifier must be 43-128 characters of [A-Za-z0-9._~-]')
        unless $verifier =~ m{\A[A-Za-z0-9._~-]{43,128}\z};
    $self->_grant_error('PKCE verification failed')
        unless $self->_ct_eq( $self->_pkce_s256($verifier),
        $binding->{code_challenge} );

    # A code exchange births a new family; a rotation inherits one.
    return $self->_issue_token_pair(
        { %$binding, family_id => $self->_random_token(16) } );
}

sub refresh ( $self, $params ) {
    my $raw = $params->{refresh_token};
    $self->_grant_error('refresh_token is required')
        unless defined $raw && length $raw;

    my $result = $self->store->rotate_refresh_token( $self->_hash_token($raw) );
    $self->_grant_error('unknown or revoked refresh token') unless $result;

    # RFC 9700: a replay means the chain is compromised and we cannot tell the
    # legitimate client from the attacker, so the whole family goes. Revoke
    # before erroring, and let a failing revoke_family surface as a 500: a
    # Store that cannot revoke is broken, and answering invalid_grant while
    # leaving the family alive fails the wrong way.
    if ( $result->{reused} ) {
        Carp::croak 'internal: reused refresh token binding has no family_id'
            unless defined $result->{binding}{family_id}
            && length $result->{binding}{family_id};
        $self->store->revoke_family( $result->{binding}{family_id} );
        # Same error and description as an unknown token: telling an attacker
        # that reuse was detected confirms they hold a real token.
        $self->_grant_error('unknown or revoked refresh token');
    }

    my $binding = $result->{binding};

    # Mirror the code-exchange client binding check (RFC 6749 6).
    if ( defined $params->{client_id} && length $params->{client_id} ) {
        $self->_grant_error('client_id mismatch')
            unless $params->{client_id} eq $binding->{client_id};
    }

    return $self->_issue_token_pair($binding);
}

sub _invalid_metadata ( $self, $desc ) {
    Catalyst::Plugin::OAuth2::AuthorizationServer::Error->throw(
        error             => 'invalid_client_metadata',
        error_description => $desc,
        http_status       => 400,
    );
}

sub metadata_document ( $self ) {
    my %doc = (
        issuer                                => $self->issuer,
        authorization_endpoint                => $self->authorize_endpoint,
        token_endpoint                        => $self->token_endpoint,
        registration_endpoint                 => $self->registration_endpoint,
        response_types_supported              => ['code'],
        grant_types_supported                 => [ 'authorization_code', 'refresh_token' ],
        code_challenge_methods_supported      => ['S256'],
        token_endpoint_auth_methods_supported => ['none'],
    );
    $doc{scopes_supported} = $self->scopes_supported if $self->scopes_supported;
    return \%doc;
}

sub register_client ( $self, $metadata ) {
    my $uris = $metadata->{redirect_uris};
    $self->_invalid_metadata('redirect_uris is required')
        unless ref $uris eq 'ARRAY' && @$uris;
    $self->_invalid_metadata('too many redirect_uris')
        if @$uris > $self->redirect_uris_max;
    for my $u (@$uris) {
        $self->_invalid_metadata('redirect_uri not a string')
            if ref $u || !defined $u || !length $u;
        $self->_invalid_metadata('redirect_uri too long')
            if length $u > $self->redirect_uri_max_length;

        my $parsed = URI->new($u);
        my $scheme = lc( $parsed->scheme // '' );
        my $ok_scheme =
              $scheme eq 'https' ? 1



( run in 3.392 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )