ory / hydra

The most scalable and customizable OpenID Certified™ OpenID Connect and OAuth Provider on the market. Become an OpenID Connect and OAuth2 Provider over night. Broad support for related RFCs. Written in Go, cloud native, headless, API-first. Available as a service on Ory Network and for self-hosters.
https://www.ory.sh/?utm_source=github&utm_medium=banner&utm_campaign=hydra
Apache License 2.0
15.53k stars 1.49k forks source link

core: add api key flow #234

Closed aeneasr closed 7 years ago

aeneasr commented 8 years ago

Hydra should support issuance of API tokens which are bound to a domain, a iOS namespace or an android namespace and targeted at services that require identification of public clients. One possibility would be to extend OAuth2 with a protocol (I could not find a spec for API tokens) or add an enpoint for it.

If OAuth2 is to be extended, the OAuth2 client will be the subject of the token. Upon request a namespace request var should be included containing a list of domans/ios/android namespaces.

API tokens are used for google maps and are used for quota checking and similar.

Here is an exemplary flow of creating such API Tokens on GCP.

1. Choose from options ak1

2. Key created ak2

3. Update key ak3

4. Key overview ak4

aeneasr commented 8 years ago

I don't think that extending OAuth2 is smart. OAuth2 is for authorizing third party apps, web/mobile api keys are for authentication of public clients and quota checking and similar.

I think it should be possible to validate these tokens using the token valid warden endpoint. Tokens can be issued by any subject that is allowed to do so by policies. The endpoint would reside in:

POST /api-keys/web/
POST /api-keys/ios/
POST /api-keys/android/

The key will be matched against an url (web) or namespace (ios, android).

I am in the process of figuring out how to validate those tokens:

  1. warden: using existing infrastructure is good, but api keys are something very different from access tokens. api keys are used for quota checking (for example) whilst access tokens are used for higher priviledged functions. It would be intransparent for developers when an API key was used and when a access token was used. This could lead to serious vulnerabilities caused by tiny mistakes. probably the core reason why extending oauth2 doesn't make sense.
  2. new endpoint: adding another validation method sucks but it would be clear to the developer what's going on. it would require a lot of boilerplate too
aeneasr commented 8 years ago

Regarding https://github.com/ory-am/hydra/issues/234#issuecomment-243002331 I am not 100% sure. One thing that comes with API tokens is that users are usually allowed to update allowed hosts and ios/andorid namespaces without having to change the API token everywhere.

In extension to above, API tokens are delegated authorization rights as well. They could, based on policies, allow user agents to do write requests as well (think anonymous comments). Using the existing OAuth2 infrastructure would additionally allow to leverage OAuth2 Access Token Validation as well as existing Access Token Issuance. By setting the TTL high (e.g. 50 years), they would basically never run out of validity and they could be revoked once #223 lands.

API Tokens could be, for example, exchanged by ID Tokens

POST /oauth2/token
Authorization: basic

grant_type=api_key
platform=web
namespaces=["foo", "bar"]
id_token=<id-token>

and modified through a new endpoint

POST /oauth2/api-keys/<key>

platform=ios
namespaces=["foo", "bar"]

One question to answer is if api keys can be issued with an id_token only, or if clients can issue those as well. I can currently not see any problems with allowing both, although restricting on id_token and opening up later would be the safer route.

Another thing that I have not solved yet is, the token's subject. There are two things to consider:

Validating the keys could be achieved using the LocalWarden:

The resulting access token could have a fixed subject (rn:hydra:oauth2:api-token) and carry the ID Token claims as part of it's own extra claims. This would allow for simple to set up policy checks and to identify the original user. The TokenAllowed method would then first check if the api token is allowed to do the stuff and then if the subject is too.

Alternatively, it could be achieved through policy conditions:

Doing both at the same time (checking what public API keys are allowed to do and checking what the user is allowed to do) could be done through policy conditions. This would however require the TokenAllowed method to append to the context if it encounters an API key. This does not feel very transparent.

aeneasr commented 8 years ago

I do not think that the following statement holds:

Another thing that I have not solved yet is, the token's subject. There are two things to consider:

  • We need to be able to distinguish API Keys from regular Access Tokens. Otherwise, API Keys would receive the same rights that their owners have.
  • Additionally to checking if the API Key as such is allowed to perform an action, we also need to check if their owner is allowed to perform that action.

The reason being that we want to limit the scope of an API Key, not distinguish in the resource server what to do if we encounter such a token. So instead of the things written above, API Keys should be issued to a limited set of scopes, which are always validated when looking up the token. For example:

We want users to be able to issue api keys in order to

  1. download images
  2. fetch some other data

Both of these endpoints have a certain scope:

  1. images.download
  2. data.read

Other endpoints, like uploading images would have different scopes like images.upload.

I think that the distinction here is: Policy lookups are used to check if a subject is allowed to do something. An API Key is issued on behalf of a subject, it is not a subject itself. Instead, the API Key carries a subset of to the subject's capabilities.

If this logic holds, it would make sense to have API Keys as an extension to OAuth2.

aeneasr commented 7 years ago

This is now being formalised as https://github.com/ory-am/fosite/wiki/OAuth2-API-Key-Grant-Type-Draft

aeneasr commented 7 years ago

Some initial spec:

package oauth2

import (
    "net/http"

    "fmt"

    "github.com/ory-am/fosite"
    "github.com/pkg/errors"
    "golang.org/x/net/context"
    "time"
    "github.com/ory-am/fosite/compose"
    "github.com/ory-am/fosite/handler/oauth2"
)

// this feature is experimental, do not use it.

func OAuth2APIKeyGrantFactory(config *compose.Config, storage interface{}, strategy interface{}) interface{} {
    return &APIKeyGrantHandler{
        HandleHelper: &oauth2.HandleHelper{
            AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy),
            AccessTokenStorage:  storage.(oauth2.AccessTokenStorage),
            AccessTokenLifespan: time.Hour * 24 * 31 * 12 * 100,
        },
        ScopeStrategy: fosite.HierarchicScopeStrategy,
    }
}

type APIKeyGrantHandler struct {
    *oauth2.HandleHelper
    ScopeStrategy fosite.ScopeStrategy
}

func (c *APIKeyGrantHandler) HandleTokenEndpointRequest(_ context.Context, r *http.Request, request fosite.AccessRequester) error {
    if !request.GetGrantTypes().Exact("api_key") {
        return errors.Wrap(fosite.ErrUnknownRequest, "")
    }

    client := request.GetClient()
    for _, scope := range request.GetRequestedScopes() {
        if !c.ScopeStrategy(client.GetScopes(), scope) {
            return errors.Wrap(fosite.ErrInvalidScope, fmt.Sprintf("The client is not allowed to request scope %s", scope))
        }
    }

    // The client MUST authenticate with the authorization server as described in Section 3.2.1.
    // This requirement is already fulfilled because fosite requries all token requests to be authenticated as described
    // in https://tools.ietf.org/html/rfc6749#section-3.2.1
    if client.IsPublic() {
        return errors.Wrap(fosite.ErrInvalidGrant, "The client is public and thus not allowed to use grant type api_key")
    }
    // if the client is not public, he has already been authenticated by the access request handler.

    request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().Add(c.AccessTokenLifespan))
    return nil
}

func (c *APIKeyGrantHandler) PopulateTokenEndpointResponse(ctx context.Context, r *http.Request, request fosite.AccessRequester, response fosite.AccessResponder) error {
    if !request.GetGrantTypes().Exact("api_key") {
        return errors.Wrap(fosite.ErrUnknownRequest, "")
    }

    if !request.GetClient().GetGrantTypes().Has("api_key") {
        return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type api_key")
    }

    return c.IssueAccessToken(ctx, r, request, response)
}

...still have to figure out how to check domains

aeneasr commented 7 years ago

@waynerobinson summed this up perfectly:

You just need a privileged client that can forge long-lived access tokens for specific scopes.

aeneasr commented 7 years ago

This can be solved much better with a proxy scenario. The proxy requests an oauth2 access and refresh token using authorize code and issues a handle, which is the API key. Then, when an API key is set in a request, it is replaced with the access token.

pbarker commented 7 years ago

@arekkas I have an ask for this, are you still interested in implementing it?

aeneasr commented 7 years ago

Not really, API keys are actually for identification, not authorization so it's not really in the scope of hydra