Catalyst-Plugin-OAuth2-ResourceServer

 view release on metacpan or  search on metacpan

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


# Write a WWW-Authenticate: Bearer challenge. $err is an optional ...::Error.
sub oauth_challenge ( $c, $err = undef ) {
    my $status = ( $err && $err->http_status ) ? $err->http_status : 401;
    my @p;
    push @p, sprintf( 'error="%s"', _oauth_rs_quote_hval( $err->error ) )
        if $err && defined $err->error;
    push @p,
        sprintf( 'error_description="%s"',
        _oauth_rs_quote_hval( $err->error_description ) )
        if $err && defined $err->error_description;
    push @p, sprintf( 'scope="%s"', _oauth_rs_quote_hval( $err->scope ) )
        if $err && defined $err->scope;
    if ( $status == 401 ) {
        my $cfg  = $c->_oauth_rs_config;
        my $base = ref $cfg->{resource} eq 'ARRAY'
            ? ( $cfg->{resource}[0] // '' )
            : ( $cfg->{resource} // '' );
        # An explicit override always wins; otherwise derive it, which only
        # works for an http(s) resource id.
        my $url = $cfg->{resource_metadata_url} // _oauth_rs_metadata_url($base);
        if ( defined $url && length $url ) {
            push @p,
                sprintf( 'resource_metadata="%s"', _oauth_rs_quote_hval($url) );
        }
        else {
            # Warn once per process: omitting the parameter is correct, but the
            # deployer probably wants to set resource_metadata_url explicitly.
            state $warned = 0;
            $c->log->warn( 'OAuth2 RS: cannot derive resource_metadata from a '
                    . 'non-http(s) resource identifier; set resource_metadata_url '
                    . 'explicitly to advertise it in 401 challenges' )
                if !$warned++ && $c->can('log');
        }
    }
    my $hdr = 'Bearer' . ( @p ? ' ' . join( ', ', @p ) : '' );

    my $res = $c->response;
    $res->status($status);
    $res->header( 'WWW-Authenticate' => $hdr );
    $res->header( 'Cache-Control'    => 'no-store' );
    $res->content_type('application/json');
    $res->body(
        $JSON->encode( { ( $err && defined $err->error ) ? ( error => $err->error ) : () } ) );
    return;
}

sub oauth_protect ( $c ) {
    my $auth = $c->request->header('Authorization') // '';
    # RFC 7235 auth-scheme names are case-insensitive.
    my ($jwt) = $auth =~ /\A Bearer \s+ (\S+) \z/xi;
    unless ( defined $jwt && length $jwt ) {
        # RFC 6750 section 3: a malformed *Bearer* attempt is invalid_request; an
        # absent header or a different/unsupported scheme gets a bare 401.
        if ( $auth =~ /\A \s* Bearer \b/xi ) {
            $c->oauth_challenge(
                Catalyst::Plugin::OAuth2::ResourceServer::Error->new(
                    error => 'invalid_request', http_status => 400 ) );
        }
        else {
            $c->oauth_challenge;    # no credentials -> plain 401
        }
        return 0;
    }

    # Token verification: a structured Error is a token failure (-> its 401);
    # anything else (engine misconfig, internal bug) is a 500, never invalid_token.
    my $verified = 1;
    my $claims = try {
        $c->_oauth_rs_engine->verify_token($jwt);
    }
    catch {
        if ( blessed $_
            && $_->isa('Catalyst::Plugin::OAuth2::ResourceServer::Error') )
        {
            $c->oauth_challenge($_);
        }
        else {
            # Deliberately a fixed string: this path is credential-adjacent and
            # the exception could carry token material.
            $c->log->error('OAuth2 RS: token verification failed')
                if $c->can('log');
            $c->_oauth_server_error;
        }
        $verified = 0;
        undef;
    };
    return 0 unless $verified;

    # Subject resolution is an app hook; a die in it is an internal error (500),
    # NOT an invalid_token. An undef return means the subject is rejected (401).
    my $identity;
    if ( $c->can('oauth_resolve_subject') ) {
        my $resolved = 1;
        $identity = try {
            $c->oauth_resolve_subject($claims);
        }
        catch {
            # Fixed string for the same reason as the verify_token catch above:
            # the hook is handed the verified claims, so an exception raised in
            # it can carry them into the log. The host app is better placed to
            # log its own failure detail than we are to guess what is safe.
            $c->log->error('OAuth2 RS: oauth_resolve_subject died')
                if $c->can('log');
            $c->_oauth_server_error;
            $resolved = 0;
            undef;
        };
        return 0 unless $resolved;
        unless ($identity) {
            # subject rejected -> generic 401 (no account-state leak)
            $c->oauth_challenge(
                Catalyst::Plugin::OAuth2::ResourceServer::Error->new(
                    error => 'invalid_token', http_status => 401 ) );
            return 0;
        }
    }

    $c->stash->{$SLOT} = { claims => $claims, identity => $identity };
    return 1;
}



( run in 0.798 second using v1.01-cache-2.11-cpan-3f962061493 )