The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Net::OAuth2::Scheme - Token scheme definition framework for OAuth 2.0

VERSION

version 0.02

SYNOPSIS

Exactly how the code would look depends on the respective server frameworks in use and we're trying to be agnostic about that, but...

  our %access_options = (
    transport => 'bearer',
    format => 'bearer_handle',
    vtable => 'shared_cache',
    cache => Cache::Memcached->new(
      # parameters for resource/authserver shared cache
      ...
    ),
    # see Net::OAuth2::Scheme::Factory for other possibilities
  );

  ##
  ## Within the Client Implementation
  ##

  our $access_scheme = Net::OAuth2::Scheme->new
    (%access_options, context => 'client');

  ... obtain authorization grant
  ... send token request

  # receive token
  #
  my %params = ... decode from JSON, URI fragment, etc...
  my ($error, @token) = $access_scheme->token_accept(%params)
  ... complain if $error

  # use token
  #
  my $request = HTTP::Request->new
     (GET => 'http://resource.example.com?resource=...&api=...&stuff=...');
  ($error, $request) = $access_scheme->http_insert($request, @token);
  ... complain if $error
  ... send $request

  ##
  ## Within the Authorization Server Implementation
  ##

  our $access_scheme =
    Net::OAuth2::Scheme->new
     (%access_options, context => 'auth_server');

  our $refresh_scheme =
    Net::OAuth2::Scheme->new
     (usage => 'refresh', ... options ... );

  # create tokens
  #
  ($error, my @token) =
    $access_scheme->token_create($now=time(), 900, ...);
    ... complain if $error

  ($error, my $refresh) =
    $refresh_scheme->token_create($now, 86400, ...);
    ... complain if $error
  }

  # issue tokens
  #
  ...respond( access_token => @token, refresh_token => $refresh );

  ##
  ## Within the Resource Server Implementation
  ##

  our $access_scheme = Net::OAuth2::Scheme->new
    (%access_options, context => 'resource_server');

  HANDLER for resource endpoint = sub {
     my $psgi_env = ... get PSGI env via whatever means

     # extract tokens from request
     #
     my ($error, @tokens_found) = $access_scheme->psgi_extract($psgi_env);
     ... complain if $error
     ... deal with (@tokens_found != 1) as appropriate
     my @token = @{$tokens_found[0]};

     # validate token
     #
     my ($error, $issue_time, $expires_in, @bindings) =
       $access_scheme->token_validate(@token);

     ... check $error
     ... check $issue_time + $expires_in vs. time()
     ... check @bindings
     ... perform API actions
  }

If one is using an "authorization server push"-style vtable, the code for that will also need to include something like

  %access_options = (... vtable => 'authserv_push' ...)

  ##
  ## within the Authorization Server implementation
  ##

  our $access_scheme =
    Net::OAuth2::Scheme->new
     (%access_options,
      context => 'auth_server',
      vtable_push => \&my_vtable_push,
     );

  sub my_vtable_push {
    my @new_entry = @_
    ... send serialization of @new_entry to authserv_push endpoint
    return ($error) if ... something bad happened
    return ()
  }

  ##
  ## within the Resource Server implementation
  ##

  our $access_scheme =
    Net::OAuth2::Scheme->new
     (%access_options, context => 'resource_server');

  HANDLER for authserv_push endpoint... = sub {
    ... authenticate authorization server
    my @new_entry = ... unserialize from request;
    my ($error) = $access_scheme->vtable_pushed(@new_entry);
    ... return error response if $error
    ... return success
  }

and if one is using an "resource server pull"-style vtable, the code for that will need to include something like

  %access_options = (... vtable => 'resource_pull' ...)

  ##
  ## within the Authorization Server implementation
  ##

  our $access_scheme =
    Net::OAuth2::Scheme->new
     (%access_options, context => 'auth_server');

  HANDLER for resource_pull endpoint ... = sub {
    ... authenticate resource server
    my @pull_query = ... unserialize from request
    my @pull_response = $access_scheme->vtable_dump(@pull_query);
    ... return response with serialization of @pull_response
  }

  ##
  ## within the Resource Server implementation
  ##

  our $access_scheme =
    Net::OAuth2::Scheme->new
     (%access_options,
      context => 'resource_server',
      vtable_pull => \&my_vtable_pull,
     );

  sub my_vtable_pull {
    my @pull_query = @_;
    ... send serialization of @pull_query to resource_pull endpoint
    my @pull_response = ... unserialize from response
    return @pull_response;
  }

DESCRIPTION

A token scheme is a set of specifications for some or all of the following

  • token transport method (http headers vs. body or URI parameters)

  • token format/encoding (handle vs. assertion vs. something else) including specification of how much of the binding information is included with the token vs. sent out of band to the resource server directly

  • communication model ("validator table" a.k.a. "vtable") for sharing token validation secrets and out-of-band binding information between the authorization server and the resource server

  • ID/key generation for the vtable,

as specialized for

  • usage (i.e., access token vs. refresh token vs. authorization code)

  • resource (i.e., a resource endpoint or family thereof that will be honoring the tokens produced by this scheme)

  • client profile/deployment (i.e., if there are distinct groups of clients using the same resource that require different styles of token for whatever reason), and

  • implementation context (client vs. authorization server vs. resource server)

The methods on the scheme object are primarily the methods for producing and handling tokens in the various stages of the token lifecycle, i.e.,

  • an authorization server calls token_create to issue the token and send validation information to the resource server(s) as needed

  • a client applies token_accept to the received token to determine (to the extent possible) whether it is of the expected scheme and then save whatever needs to be saved for later use

  • a client uses http_insert and the saved token information to insert a token into a resource API message as authorization,

  • a resource server does psgi_extract to obtain whatever tokens were present, and then token_validate to verify them and obtain their respective binding information. These are two separate methods because (1) handling of multiple apparent tokens in a message will depend on the resource API and is thus outside the scope of these modules, and (2) for refresh tokens and authorization codes, psgi_extract is not actually needed.

but there will sometimes be additional hooks a needed by the communication model (see discussion of vtable_push and vtable_pull below).

CONSTRUCTOR

new

 $scheme = new(%scheme_options);
 $scheme = new(factory => $factory_class, %scheme_options);

See Net::OAuth2::Scheme::Factory, the default factory class, for what can be in %scheme_options.

Use the second form if you want to substitute your own $factory_class; note that if you use this option, it must appear first.

Everything that follows describes the behavior of the methods produced by the default factory class.

METHODS

The parameter and return values that are used in common amongst the various scheme object methods are as follows:

$issue_time

time of token issue in seconds UTC since The Epoch (midnight, January 1, 1970)

$expires_in

number of seconds after $issue_time that token expires

@bindings

an arbitrary sequence of string values that are bound into the token.

For the purposes of this module these values are opaque and up to the module user. Doubtless an OAuth2 implementation will almost certainly be including at least resource_id, client_id, and scope...

$request_out

an outgoing request as might be composed by a user agent or application, in the form of an HTTP::Request object or something with a similar interface.

$request_in

an incoming request as received by a server URI handler, in the form of a PSGI environment hash

(see HTTP::Message::PSGI for converting HTTP::Request objects to PSGI environments, and the various Plack::Handler::* modules for how to obtain PSGI environments from other kinds of server request objects, e.g., Apache(2)::Request, CGI, etc...)

@token_as_issued

the token string (access_token value from a token response) followed by the sequence of alternating keyword-value pairs that comprise the token as initially sent by the authorization server to the client.

The keywords used here may include token_type and any extension parameters registered for use in OAuth2 token responses. In general the full list represents what is needed in order to construct an access request. All values are as they appear in a successful token or authorization endpoint response (i.e., prior to being encoded into a JSON structure or URI fragment on the authorization server, or, equivalently, after such decoding on the client side).

The keywords expires_in, scope, refresh_token, and access_token are never included.

For refresh tokens and authorization codes, @token_as_issued will always be a one-element list consisting of a single string value (i.e., the refresh_token parameter from a token response or the code parameter from an authorization response)

@non_token_params

the keyword-value pairs corresponding to the expires_in, scope, refresh_token and any other parameters (e.g.,extension, local variation, etc...) received in a token response that are not needed in order to construct an access request using this token.

@token_as_saved

the token string plus alternating keyword-value pairs in the form that the token is to be saved on the client.

This may include additional client-side data as required by the token scheme (e.g., http_hmac requires the receive time). At the discretion of the client implementer, some or all of @non_token_params can also be included as well.

@token_as_used

the token string plus alternating keyword-value pairs in the form that the token gets sent to the resource server. Here, the keywords will generally refer to additional Authorization header attributes, body parameters, or URI parameters (or something else if anyone comes up with some other place to stash tokens in an HTTP request) required by the transport scheme in use; these keywords need not have anything to do with the keywords that appear in @token_as_issued or @token_as_saved.

For refresh tokens and authorization codes, @token_as_issued and @token_as_used are one-element lists consisting of a single string value.

$error

in return values will be undef when the method call succeeds, and otherwise will be some true string value indicating what went wrong when the method call fails.

The following methods will be defined on token scheme objects, depending on the usage and implementation context chosen:

token_create [Authorization Server]

 ($error, @token_as_issued) =
   scheme->token_create($issue_time, $expires_in, @bindings)

creates a new token in the form to be sent to the client. As a side effect this also communicates any necessary secrets and perhaps also some subset of the expiration and binding information to the resource server as needed.

Questions of token format, whether (and which) bindings are physically included with the token as sent to the client vs. communicated separately to the resource server, and how such communication takes place are determined by the format and vtable specifications chosen for this token scheme.

token_accept [Client]

 ($error, @token_as_saved)
   = scheme->token_accept(@token_as_issued, @non_token_params)
  • checks that the token_type parameter is as expected for this token scheme.

  • includes in @token_as_saved, additional client-side information (e.g., the time of receipt for http_hmac tokens) that may be needed to construct access requests,

  • includes some or all of @non_token_params as determined by the option settings accept_keep and accept_remove. Note that the @non_token_params supplied to this call can be a (possibly empty) subset of the originally received @non_token_params (i.e., it's okay to remove these parameters beforehand if you want).

Clients can simultaneously accomodate multiple token transport schemes provided either each expected token_type value corresponds to at most one specified token scheme, e.g.,

  my ($error, $use_scheme, @token_as_saved);
  for my $scheme ($bearer_scheme, $http_hmac_scheme, $whatever...) {
     ($error, @token_as_saved)
       = $scheme->token_accept(@token_as_issued);
     unless ($error) {
         $use_scheme = $scheme;
         last;
     }
  }
  unless ($use_scheme) { ... complain... }

or you have some other means of identifying received tokens (e.g., some other local-extension URI parameter documented by the authorization server people tells you which it is)

http_insert [Client]

 ($error, $request_out)
  = $scheme->http_insert($request_out, @token_as_saved)

converts @token_as_saved to @token_as_used — silently ignoring any @non_token_params that might be present — then modifies (in-place) the outgoing request so as to include @token_as_used as authorization, returning the modified request. This may either add headers, post-body parameters, or uri parameters as per the transport specification for this token scheme.

psgi_extract [Resource Server]

 ($error, [@token_as_used],...) = $scheme->psgi_extract($request_in)

extracts all apparent tokens present in an incoming request that conform to this token scheme's transport specification.

Ideally, there would be at most one valid token in any given request, however, other headers or parameters may, depending on how the resource API is structured, spuriously match the token transport specification and we won't find this out until we attempt to validate the resulting "tokens" (not that this should happen with a well-designed API, but there may be legacies and compromises to contend with...)

It may also be that one may wish for a given resource API to accept multiple tokens in certain situations. If you go this route, it is strongly recommended that there be a fixed, small limit on number of tokens that may be included in any request — otherwise you risk providing an attacker an easy means of brute-force search to forge/discover token values.

token_validate [Resource Server, Refresh Tokens and Authcodes]

 ($error, $issue_time, $expires_in, @bindings)
   = $scheme->token_validate(@token_as_used);

Decodes the token, retrieves expiration and binding information, and verifies any signature/hmac-values that may be included in the token format.

The caller is responsible for deciding whether/how to observe the expiration time and for checking correctness of binding values.

vtable_pushed [Resource Server]

 ($error) = $scheme->vtable_pushed(@push_entry)

For use by resource server authserv_push handlers (see ...).

Here @push_entry is an opaque sequence of strings extracted from the authserv_push message constructed and sent by vtable_push.

vtable_dump [Authorization Server]

 @pull_response = $access_scheme->vtable_dump(@pull_query)

For use by authorization server resource_pull handlers (see ...).

Here @pull_query is an opaque sequence of strings extracted from the pull request constructed and sent by vtable_pull and @pull_response is the corresponding opaque sequence to be included in the response and returned from vtable_pull on the resource server side. Note that @pull_response may contain an error indication, but if so, that should be handled by the resource server.

AUTHOR

Roger Crew <crew@cs.stanford.edu>

COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Roger Crew.

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