Skip to content

Instantly share code, notes, and snippets.

@powerman
Created June 25, 2022 18:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save powerman/ccabf6f29be5054cbace9743771e1ec1 to your computer and use it in GitHub Desktop.
Save powerman/ccabf6f29be5054cbace9743771e1ec1 to your computer and use it in GitHub Desktop.
Recommendations on secure implementation of OAuth 2.0 (server and client)

OAuth 2.0

Abstract

Recommendations on secure implementation of OAuth 2.0 (server and client):

  • extra limitations compatible with specification and critical for security are described as required

  • extra features compatible with specification and critical for security are described as required

  • required extensions to specification are marked [EXTENSION]

  • features non-critical for security are marked [OPTIONAL]

  • some optional features are forbidden or required to increase overall control and simplify implementation

Whenever possible all limitations/extensions to specification are described with rationale and/or possible attack scenario.

Background & Terminology

Here is how terms used in this document map to OAuth roles:

Server

authorization server - the server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization (most likely we’ll implement it as a group of authentication and authorization services)

Client

client - an application making protected resource requests on behalf of the resource owner and with its authorization (our own public website is also just a client)

User

resource owner - a person (end-user) capable of granting access to a protected resource

Server

Client registration

All clients should be registered on the server. Usually server should provide a web interface with these features for owners of client applications:

  • Register.

  • Login.

  • Add/delete his client applications.

  • Edit details about his client applications.

  • Join some of his client applications into a group, to share access provided by user to one of client applications in a group with all others in same group. All clients in a group should share same description (name, url, logo). [OPTIONAL]

  • In case client will leak (occasionally or because it was hacked) many tokens we should provide a feature to allow client to revoke all his current active tokens and change client_secret.

    • this application type may not have client_secret

    • it’s usually bad idea to change client_secret for desktop/mobile/limited applications because it’s usually hardcoded and thus all users will have to update this application after changing client_secret

  • View names for all scope allowed for each of his client applications.

Client details should provide this information:

  • Description (required to let users identify this client):

    • name

    • url

    • logo [OPTIONAL]

  • Type of client (it actually just change which ones of other options will be allowed to select, so it’s pure usability feature [OPTIONAL]):

    • server-side web application

      • it’s recommended to require list of IP addresses of servers with this application and allow access only from these IP addresses - to make sure this type of client won’t be used instead of mobile or desktop applications

    • browser application (web site)

    • mobile application

    • desktop application

    • limited application (for devices with limited capabilities, like TV)

  • List of allowed redirect_uri (to protect against many types of attack).

    • server-side/browser applications are allowed to use only https://

    • mobile/desktop applications are allowed to use any url schemes

      • it’s recommended to disallow http:// and https:// except for the localhost

    • limited applications doesn’t use redirect_uri, instead they use non-standard way to get access_token which involve usual browser (either application get the code and show it to user so he can then enter this code on server’s website, or server’s website show simple numeric code to user which usually possible to enter even on limited devices)

    • redirect_uri may contain parameters, but not a fragment

  • Allowed response_type (needed to avoid lowering security whenever possible and prevent simple intercepting of access_token).

    • server-side applications should not be allowed to choose "token"

    • each redirect_uri must have only one allowed response_type

  • Is application needs refresh_token (needed to avoid lowering security whenever possible).

    • not allowed for browser applications

    • in case application need it then this should be handled as extra scope - i.e. user should grant it just like all other permissions

      • if client details was updated and needs in refresh_token was added then it should be handled as asking user to grant extra permissions

  • Auto-generated client_id.

  • Auto-generated client_secret.

    • allowed only for server-side applications

  • Is grant_type=password is allowed (admin should enable this only for our own clients).

To protect against phishing we should:

  • Block registration of clients with similar names.

    • This can be easily hacked using Unicode symbols, so it’s better to allow only English letters and numbers in client names, remove extra spaces, take in account similar letters/numbers like "I" and "l".

  • Disallow using urls with same domain by different client owners.

  • Make sure this client owner really control domain used in url.

    • We can ask him to create unique file on website or send email to postmaster@ this domain.

  • If possible, it makes sense to manually pre-moderate added and modified client descriptions.

Endpoint: authorization

  • Must support GET and may support POST.

    • If any parameter provided more than once (i.e. it has several values) then return error.

    • Unknown parameters must be ignored.

    • Parameters with empty values must be ignored.

  • Must accept client_id and client_secret using Authorization: Basic (only for clients with client_secret) or using POST params (clients without client_secret should not include this param).

    • If both ways are used in same request then return error.

grant type "authorization code"

Parameters
response_type

"code"

client_id
  • client must be registered

redirect_uri
  • must match one of registered values for this client_id

scope

permissions asked by client

  • consists of case-sensitive strings of 0x21-0x7E excluding " and \ symbols, separated by spaces

state

opaque value

  • without it client is vulnerable to CSRF

  • minimum length is 6 bytes

  • it’s recommended to ensure client use non-constant value - remember few last used values and reject same values in next requests

Reply: access granted

Reply is HTTP 302 Found redirect to redirect_uri with these parameters appended:

code

random value generated by server

  • it’s recommended to limit lifetime of this code by 10 minutes

  • it should be tied to client_id and redirect_uri used to get this code

  • if same code will be used more than once we should immediately revoke all tokens issued using same code

state

equal to parameter state in request

After parameters we should append empty fragment # (FIXME or #_=_ - this value was used by Facebook, maybe just # is not enough for all browsers to replace current fragment value, or maybe it’s just a cargo cult).

Note
This needed in case our OAuth server is also an OAuth client (for ex. allowing our users to "login with Facebook") - in this case chain of redirects may happens: from Facebook to us with providing user’s auth code/token in fragment, and then from us to our client with providing user’s auth in parameters. If second redirect won’t have fragment then browser will copy fragment part of first redirect into second, thus leaking user’s Facebook auth to our client.
Reply: wrong client_id or redirect_uri error

Reply is HTTP 200 OK with error shown to user.

Reply: other errors

Reply is HTTP 302 Found redirect to redirect_uri with these parameters appended:

error
  • "invalid_request" - something wrong with request parameters

  • "unauthorized_client" - client isn’t pass pre-moderation yet or is blocked

    • it’s better to show such error to user with 200 OK instead

  • "access_denied" - user doesn’t grant access to this client

  • "unsupported_response_type" - server doesn’t support requests with response_type=code

  • "invalid_scope" - bad scope value

  • "server_error" - in place of 500 Internal Server Error

  • "temporarily_unavailable" - in place of 503 Service Unavailable

error_description
  • optional error description in English

error_uri
  • optional link to error description

state

equal to parameter state in request

grant type "implicit"

Parameters
response_type

"token"

All other parameters are the same as for grant type "authorization code".

Reply: access granted

Reply is HTTP 302 Found redirect to redirect_uri with these key/value pairs appended as fragment using same encoding as for url parameters (key=value&…):

access_token
  • must not be a refresh_token - it’s too unsafe to provide so powerful token with grant type "implicit"

token_type
  • case-insensitive

  • values "bearer" and "mac" documented in specification isn’t suitable for us, so we should use value of type "own url", like "https://api.cpdpro.org.uk/" (to mark this token as intended for use only in our JSON RPC 2.0 API)

expires_in
  • recommended (in seconds)

    • I’ve no idea what’s for client may use this value - tokens are expired on server, and clients must be ready to get error because of expired or revoked token at any time.

state

equal to parameter state in request

Reply: wrong client_id or redirect_uri error

Reply is HTTP 200 OK with error shown to user.

Endpoint: token

  • Must support only POST.

    • If any parameter provided more than once (i.e. it has several values) then return error.

    • Unknown parameters must be ignored.

    • Parameters with empty values must be ignored.

  • Must accept client_id and client_secret using Authorization: Basic or using POST params.

    • If both ways are used in same request then return error.

Parameters

grant type "authorization code"
grant_type

"authorization_code"

code

code returned by authorization endpoint used with grant type "authorization code"

redirect_uri

redirect_uri provided in params while requesting code from authorization endpoint

client_id

client_id provided in params while requesting code from authorization endpoint

  • absent if Authorization: Basic is used

client_secret
  • absent if Authorization: Basic is used

  • absent if there is no client_secret for this client_id

grant type "resource owner password credentials"
grant_type

"password"

  • allowed only for our own clients

username

user’s account name

password

user’s account password

scope

any existing scope

client_id
  • absent if Authorization: Basic is used

client_secret
  • absent if Authorization: Basic is used

grant type "client credentials"
grant_type

"client_credentials"

scope

same as or subset of scope allowed for this client

client_id
  • absent if Authorization: Basic is used

client_secret
  • absent if Authorization: Basic is used

grant type "refresh access_token"
grant_type

"refresh_token"

refresh_token

refresh_token returned by last request to token endpoint which returned refresh_token

scope

same as or subset of scope parameter used to get this refresh_token

client_id
  • absent if Authorization: Basic is used

client_secret
  • absent if Authorization: Basic is used

  • absent if there is no client_secret for this client_id

Reply: request is correct

Reply is HTTP 200 OK with headers "Cache-Control: no-store", "Pragma: no-cache" and json object with these keys as content:

access_token
token_type
  • case-insensitive

  • values "bearer" and "mac" documented in specification isn’t suitable for us, so we should use value of type "own url", like "https://api.cpdpro.org.uk/" (to mark this token as intended for use only in our JSON RPC 2.0 API)

expires_in
  • recommended (in seconds)

    • I’ve no idea what’s for client may use this value - tokens are expired on server, and clients must be ready to get error because of expired or revoked token at any time.

refresh_token
  • optional

  • not provided when client_id doesn’t need it (as configured by client application’s owner)

  • not provided on grant_type=client_credentials request

  • it’s recommended to not provide refresh_token for clients without client_secret

  • if provided on grant_type=refresh_token request, then:

    • this refresh_token should replace previous refresh_token (used in this request)

      • to protect against losing new refresh_token by client server may expire previous refresh_token when it get first request with refresh_token or access_token provided in this reply

    • scope for this refresh_token should be same as it was for previous refresh_token (not as scope parameter of this request)

    • server shouldn’t forget previous refresh_token, at least for some time - this allow server to detect leaked refresh_token (if it get request with outdated refresh_token)

Reply: request is wrong

Reply is HTTP 200 OK with headers "Cache-Control: no-store", "Pragma: no-cache" and json object with these keys as content:

error
  • "invalid_request" - something is wrong with request parameters

  • "invalid_client" - client authentication failed

  • "invalid_grant" - value of code or refresh_token parameter isn’t valid (for ex. expired, or should be used with another client_id or redirect_uri, revoked, etc.)

  • "unauthorized_client" - client authentication was successful, but requested grant_type isn’t allowed for this client_id

  • "unsupported_grant_type" - wrong grant_type value

  • "invalid_scope" - wrong scope value

  • "access_denied" - user doesn’t grant requested scope [EXTENSION]

error_description
  • optional error description in English

error_uri
  • optional link to error description

Incremental extending of scope

[OPTIONAL]

To make it possible for client to incrementally ask user for new permissions when they’re needed for functionality just requested by user:

  • usual check is some scope granted by user should work completely in background, without asking user to grant that scope each time

  • client should be able to provide a feature to let user change his decision and grant scope which he refused to grant previously

  • it should be possible for user to revoke some scope he already granted using server’s web interface

Note

To implement this Google uses extra parameter for authorization endpoint: approval_prompt=force (Facebook use auth_type=rerequest).

In "OpenID Connect" service Google use optional parameter prompt=none|consent:

  • if doesn’t provided then use default behaviour (ask user if requested scope isn’t granted yet)

  • if set to none then used to background scope check and never shown to user

  • if set to consent then always ask user

One more way to implement this: client may ask user to manually enable needed scope in user’s area in server’s web interface (but this harm usability).

In my opinion prompt=none|consent is the most adequate one.

Token validation

[EXTENSION]

Tokens received by browser applications should be additionally validated to protect against using tokens issued to another user:

  1. user U use two applications: usual client A and malicious client B (because it pretend to be some other client or some useful application), and login using his Google account into both applications

  2. when U login into B a hacker (owner of B) will get from Google token T for user U

  3. next hacker will run client A, it redirects to Google to log in current user, then hacker manually assemble url to client A using redirect_uri and state just used by A in this redirect and token T - and get access to user’s U account in client A

To protect against this client must check is this token was issued for this client. There are several ways to do this:

  • client may do some request to server using both his client_id and this access_token - if request fails with authentication error then this token was issued for another client

  • server may provide special API to get client_id tied to given access_token and client may call it and compare returned client_id with it’s own

  • to avoid performance issue because of extra request in two previous cases server may provide extra value together with access_token - this value should contain client_id and it should be signed by server (Google’s OpenID Connect works in this way)

This issue happens because OAuth was designed for authorization, and while it used to access user’s resources on server everything is fine. But when OAuth is misused by client for authentication and as result authorization to user’s resources on client - we got this issue.

For OAuth (not OpenID Connect!) Google provide extra endpoint /tokeninfo?access_token= which returns details about given token or error. Facebook does something similar.

Auto-detecting hacked client or leaks

Attempt to reuse authorization code or use expired refresh_token is a sign of a leak. In first case server should revoke all tokens issued using that code. In second case server should block access for corresponding client_id until that client’s owner log in and get new client_secret.

Vendor library for clients

We may implement our own library for Javascript, Perl, Python, etc. which implements access to our API (hiding all details of our OAuth 2.0 and JSON RPC 2.0 implementation) which will guarantee protection against some security issues.

Resource limits

Server should be careful to avoid allocating unlimited amount of resources. For example, Google does this: "Limits apply to the number of refresh tokens that are issued per client-user combination, and per user across all clients, and these limits are different. If your application requests enough refresh tokens to go over one of the limits, older refresh tokens stop working.".

Security

  • All sensitive operations (like involving password or scope/consent) must be protected with CSRF token.

  • It shouldn’t be possible to load server web pages (like asking for consent page) inside a frame to protect against clickjacking.

  • It may make sense to also reject to load server web pages inside an embedded web view in native mobile app (to force native app to run system browser instead of using embedded web view).

    • The particular techniques for detecting whether the page is being visited in an embedded web view vs the system browser will depend on the platform, but usually involve inspecting the user agent header.

  • For clients without client_secret server may require PKCE https://www.oauth.com/oauth2-servers/pkce/

Client

Security

  • Client must send and check returned state.

    • Protects against CSRF. If client provide a feature like "connect your account with Facebook", user is now logged in on client, and user is visiting malicious website, then hacker owning that malicious website may send his own code or access_token issued by Facebook to client’s redirect_uri and get hacker’s Facebook connected to user’s account on client.

  • After client has processed parameters sent by server to redirect_uri it must redirect to some other internal url to protect against leaking code or access_token from current page’s url to any 3rd-party javascript/css/image loaded on returned web page.

    • Such sensitive urls on client must avoid loading any 3rd-party javascript/css/image or other resources.

  • Client must validate received access_token to make sure it was issues to this client.

    • This isn’t required in case client use only response_type=code and doesn’t support response_type=token at all).

  • It may make sense to always use response_type=code, even for client types without client_secret - this let server to detect leaked code and invalidate related access_token plus it’ll let both client and server ensure these code and access_token are for this client_id.

  • In case client has XSS vulnerability (most client can’t be 100% sure they hasn’t one) some extra protection needed:

    • If hacker can get client_secret (desktop/mobile clients, or if he has hacked server-side part of client, etc.) then he can modify state to activate CSRF protection on client and have client ignore this request and don’t use issued code - which let hacker to use that code. To protect against this client must use code in any case, but then drop received access_token if CSRF protection has detected wrong state.

    • If state is stored in some accessible from Javascript place (like in cookies) then it must be an one-time value and must be removed from that place right after receiving a reply - to protect against attempt to get or set it by hacker to bypass CSRF protection.

  • All sensitive operations (like "connect with Facebook") must be protected with CSRF token.

    • If server doesn’t have CSRF protection on login page (for ex. Facebook refuse to implement it to not break existing applications), then malicious website can in background login user visiting that site into Facebook account of hacker owning that site. Next it can use "connect your account with Facebook" feature of any 3rd-party client application to connect user’s account on that client to hacker’s Facebook account - also in background, in invisible for user way, when user is on malicious website. As result, hacker will get access to user’s account on that client.

    • It is also recommended to add extra step (after receiving access_token but before actually connecting some provider’s account) asking user is he really want to connect it, showing both provider name (Facebook) and name of user’s account on that provider (because there are lots of attacks such as cookie forcing, login/logout CSRF, etc. that can silently relogin user into malicious account on the provider).

Note
It should be safe to use Google’s OpenID Connect for authentication.

In general, if client is really bother about it’s own security, it should test each OAuth server for vulnerabilities before begin using them: is server allow using redirect_uri which differs from configured for this client (for ex. with added extra path, parameters, fragment, using subdomain like www. or another http/https scheme), is server check validity of all parameters while exchanging code to access_token, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment