Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OAuth2 API authorization method #57

Closed
jwhitlock opened this issue Feb 24, 2021 · 7 comments
Closed

Implement OAuth2 API authorization method #57

jwhitlock opened this issue Feb 24, 2021 · 7 comments
Assignees

Comments

@jwhitlock
Copy link
Contributor

jwhitlock commented Feb 24, 2021

Our current design is that API access requires authorization, and we expect a small number of production clients (Basket, a Mozilla Foundation client, and (maybe) a bulk CTMS importer). We also want to provide credentials to developers and integrators that are not used in production, and a way to easily generate credentials tied to a local development environment.

FastAPI has Security documentation that shows how to implement an OAuth2 workflow, and integrates this with the Swagger API docs. There are several OAuth2 clients, such as requests-oauthlib.

Are there other authorization frameworks we should consider?

Part of JIRA-OSS-302.

@bsieber-mozilla
Copy link
Contributor

I'm curious if the SRE's have any suggestion for us here; otherwise, I was also thinking FastAPI's security docs had a path forward.

@jwhitlock jwhitlock changed the title Pick an API authorization method Implement OAuth2 API authorization method Feb 26, 2021
@jwhitlock
Copy link
Contributor Author

@sciurus asked the question in our security operations Slack, and the feedback was:

  • SSO is usually the right answer
  • HAWK can be considered deprecated
  • Based on the service description, OAuth2 refresh or bearer tokens is probably fine
  • We should schedule a Rapid Risk Assessment (RRA). That document says "run the first RRA during the design or architecture phase", so we can do that soon. We're tracking it in the backlog, and I'm aiming to make the RRA request next week.

I'm going to proceed with OAuth2, so we have some application-level protection of data, and we can evaluate if it is the right solution in the context of an RRA.

@jwhitlock jwhitlock self-assigned this Feb 26, 2021
@jwhitlock
Copy link
Contributor Author

I've worked my way through 4 of 5 steps in the FastAPI tutorial. It implements resource owners password credentials grant, where you pass a username and password to the /token endpoint, and it responds with an access token that can be used when calling the API. This might be appropriate for (legacy) user-facing services that also have an API, but 1) we don't intend to offer user accounts, and 2) there are better ways to implement user-facing authentication, such as OpenID Connect.

We expect a small number of server-based clients, configured with a per-deployment access key that is applied using similar secret systems to database credentials. The matching OAuth2 workflow is a client credentials grant, where a client trades auth credentials for a short-lived (such as 1 hour) access token, without a refresh token.

The workflow, in development and production, would be:

  1. Run a script to generate new API credentials. I'd expect something like:

    ctms/bin/generate_credentials.py --name=basket [email protected]
    

    This would create a new entry in the api_client table, and output:

    Your credentials are:
    
    client_id: id_basket_cf64d174
    client_secret: secret_915bf0991f62440dbd4913fd22076454
    

    A hash of the client_secret is stored, rather than the exact value. I used a UUID for a placeholder random string, but the actual implementation will use random bits encoded with base64 or similar.

  2. The API client POSTs the credentials to /token. like:

    POST /token HTTP/1.1
    Host: ctms.example.com
    Authorization: Basic aWRfYmFza2V0X2NmNjRkMTc0OnNlY3JldF85MTViZjA5OTFmNjI0NDBkYmQ0OTEzZmQyMjA3NjQ1NA==
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials
    

    The Authorization header is the base-64 encoded string for client_id:client_secret

    The response is an access token:

    HTTP/1.1 200 OK
    Content-Type: application/json;charset=UTF-8
    Cache-Control: no-store
    Pragma: no-cache
    
    {
      "access_token":"2YotnFZFEjr1zCsicMWpAA",
      "token_type":"bearer",
      "expires_in":3600,
    }
    

    If possible, the client stores this token, such as in a cache, so it can use it for multiple requests.

  3. The client calls the API with the token:

    GET /ctms/5a1c2f07-cdac-4238-a806-794eb53912c HTTP/1.1
    Host: ctms.example.com
    Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
    

    On expiration, a 401 Unauthorized is returned:

    HTTP/1.1 401 Unauthorized
    WWW-Authenticate: Bearer realm="ctms",
                      error="invalid_token",
                      error_description="The access token expired"
    

The cons of this process:

  • The security is not very different from basic auth with the client ID and secret as the username:password
  • It requires Basket and other callers to integrate an OAuth2 client, maybe some caching, and some retry logic around expired tokens.
  • It is possible that the OAuth2 client library has limited support for the client credentials workflow
  • FastAPI has some support for client credentials, but it still requires some custom code.

The pros of this process:

  • We're using OAuth2, and get the benefits of a software ecosystem, known security features, etc.
  • We can incrementally add other features as needed. For example, we could add scopes to distinguish between API clients and their permissions. We could add SSO integration for a user-facing API for CTMS managers or (someday) users.

@jwhitlock
Copy link
Contributor Author

I've confirmed with @pmac that Basket will be able to use OAuth2 with client credentials to access CTMS, and that other flavors of OAuth2 are used to integrate with other services.

@pmac
Copy link
Collaborator

pmac commented Mar 3, 2021

Bonus points if I can use it with a library I'm already using: requests-oauthlib

@jwhitlock
Copy link
Contributor Author

I've got in-progress code at main...jwhitlock:wip-oauth2. It adds the OAuth2 client credentials flow, and requires it for API endpoints. There's tests as well, and most tests fake authentication. The authentication uses hashing code that avoids giving away timing information, so the real authentication tests are noticeably slower.

I want to re-organize the code into logical commits, clean up docstrings, and other cleanup. I also need to write and document ctms/bin/generate_credentials.py. This should be a solid day of work, so the PR will be next week.

Bonus points if I can use it with a library I'm already using: requests-oauthlib

This is documented as Backend Application Flow using the BackendApplicationClient. However, the request-oauthlib docs mistakenly call this the "Resource Owner Client Credentials Grant Type". There is also requests/requests-oauthlib#260, which details problems with automatically refreshing a token without the regular refresh token workflow. There are other open issues, but I think my implementation will work more smoothly, since I support credentials in the Authentication header or as a form-encoded body.

The client credentials grant type matches our use case, but it is not too late to switch to or add an OAuth2 grant type that is better supported by the tools.

@jwhitlock jwhitlock added this to the OSS 2021 Q1 Sprint 5 milestone Mar 11, 2021
@jwhitlock
Copy link
Contributor Author

There's more I want to do with authentication, but let's track with new issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants