Accept
This website is using cookies. More details

Gokdag Goktepe
Cloud Engineer, Developer, A life long learner

OAuth 2.0 Authorization Code Flow & AWS Cognito

OAuth 2.0 Authorization Code Flow & AWS Cognito

The previous article introduced the OAuth 2.0 protocol, highlighting distinctions between authentication and authorization. OAuth 2.0, an industry-standard protocol, was discussed in the context of cloud-native applications, addressing security concerns by segregating client and resource owner credentials and access. The article also briefly covered the historical evolution of authorization flows.

As promised, this article delves into the key distinctions between OAuth 1.0 and OAuth 2.0. It offers an in-depth exploration of OAuth 2.0, complete with technical details and code samples. The article concludes by examining the challenges faced by companies and developers in implementing OAuth 2.0. Lastly, we explore use cases for AWS Cognito within the scope of the OAuth 2.0 Protocol.

Table of Contents

  1. OAuth 1.0 Vs. OAuth 2.0
  2. OAuth 2.0 Anatomy
  3. Authorization Code Flow for Web Applications – Confidential Clients
  4. Client credentials grant flow – Machine-to-machine
  5. OpenID Connect
  6. OpenID token validation
  7. Challenges of OAuth 2.0
  8. AWS Cognito
  9. Conclusion
  10. References

OAuth 1.0 Vs. OAuth 2.0

OAuth 1.0 drew significant inspiration from two proprietary protocols, namely, Flickr’s authorization API and Google’s AuthSub as mentioned in the first part of this article series. This decision was based on the proven and production-grade implementation experience of these two protocols. However, OAuth 2.0 emerged out of OAuth 1.0 challenges that the community and companies faced, and it is a complete rewrite of the protocol.

First, OAuth 2.0 introduces distinct roles, including the client, authorization server, resource server, and resource owner. In contrast, OAuth 1.0 employs different terminology, with the “client” referred to as the consumer, the “resource owner” simply as the user, and the “resource server” as the service provider. OAuth 1.0 lacks a clear separation between the resource server and authorization server roles.

The primary reason for the failure of many OAuth 1.0 implementations was the protocol’s demanding cryptographic requirements. OAuth 1.0’s intricate signature process proved challenging for those accustomed to simple username and password authentication for API calls. OAuth 2.0’s Bearer tokens have simplified these calls, enabling swift interactions through access tokens instead of traditional usernames and passwords.

OAuth 2.0 comprises two main components. Obtaining user authorization, resulting in an access token, and utilizing that token to make user-authorized requests. The methods for acquiring access tokens are called flows.

In OAuth 1.0, there were initially three flows for different client types, but they merged into one, causing usability issues. OAuth 2.0 addresses this by defining multiple grant types to accommodate various application types and allowing extensions for new use cases.

OAuth 1.0 faced scalability challenges for larger providers due to state management complexity, unnecessary generation of temporary credentials, and the need for long-lasting, less secure credentials. It also disrupted the typical architecture of large providers where separate authorization and API servers were used. OAuth 2.0 improves this by using client credentials only during user authorization, allowing API servers to validate access tokens independently, reducing complexity and enhancing security.

In OAuth 1.0, access tokens consist of public and private strings, with the private string used for request signing and not sent over the network. OAuth 2.0 primarily employs Bearer Tokens, a single string in the Authorization header of an HTTPS request.

OAuth 1.0 often issued long-lasting access tokens, which could create limitations for service providers wanting to allow users to revoke app access. OAuth 2.0 introduces short-lived access tokens and long-lived refresh tokens. This design enables apps to obtain new access tokens without user involvement and simplifies token revocation for servers, drawing from Yahoo!’s BBAuth and OAuth 1.0 Session Extension.

OAuth 2.0 intentionally separates the roles of the authorization server and the API server. The authorization server handles user authorization and token issuance, while the API server only accepts access tokens. These roles can exist on separate physical servers or even different domains, facilitating independent scalability. This design eliminates the need for the API server and the authorization server to share a common database, simplifying independent scaling of API servers.

In conclusion, OAuth 2.0 builds on the lessons of OAuth 1.0, introducing roles, flexible flows, enhanced security, and independent scalability.

OAuth 2.0 Anatomy

In this chapter, I will provide a detailed overview of the OAuth 2.0 protocol specifications and relate AWS Cognito features to the protocol as needed.

As mentioned in the first part of this article series, OAuth 2.0 defines several roles. The user is the resource owner, the device is the agent used by the user, the application is the OAuth client responsible for making token requests to access the user’s resources, and the API serves as the resource server where the user’s resources are hosted. Additionally, there is the authorization server, a separate entity with the sole responsibility of authorizing applications on behalf of the user. Within the scope of these roles, AWS can function as both the resource server (AWS compute and storage services) and the authorization server (AWS Cognito).

OAuth 2.0 defines two client types: Confidential clients and Public clients.

Confidential clients can securely store and use a client secret. These clients are typically server-side applications written in .Net, Java, NodeJS, etc. and can be hosted in AWS Lambda and AWS EC2.

On the other hand, Public clients cannot securely store a client secret on the client, making it exposed. This category mostly includes mobile apps and single-page applications.

A client secret is a confidential piece of information known only to the application and the authorization server. The application proves its identity with a client secret. For each registered application, it’s essential to store the public client_id and the private client_secret securely. Storing the secret in plain text is not recommended; instead, using encryption or hashing to reduce the risk of a security breach is strongly advised.

OAuth 2.0 protocol specification defines the following code flows:

  • Web application code flow – Confidential clients
  • Native Apps code flow
  • Single-page Application code flow
  • Authorization code flow for IoTs – Device flow – Device authorization grant
  • Client credentials grant flow – Machine-to-machine

We are going to see only Web application and Client credentials grant flow within the two following sub-chapters. They are two mostly used grant flows within the scope of OAuth 2.0.

Authorization Code Flow for Web Applications-Confidential Clients

The flow can be depicted as the following.

oauth-2-authorization-code-flow-diagram

Before the application sends the user to the OAuth server, it generates a random string between 43-128 characters in length, which is called the code verifier secret.

The application then takes the code verifier and calculates the SHA256 hash of it and encodes the result using Base64 URL encoding.

1
base64Url(sha256(code_verifier))

Now, the application needs to construct the link to send the user to the OAuth server. You should identify the authorization endpoint and add query string components as follows.

1
2
3
4
5
6
7
8
https://authorization-server.com/auth?
  response_type=code&              # Tells the server that you are doing authorization code flow
  client_id=CLIENT_ID&             # The id that application received when registered to the authorization server
  redirect_uri=REDIRECT_URI&       # An Uri that authorization server redirects the user back to the application
  scope=photos&                    # Defines what the application can access from the user's resources
  state=XXXXXX&                    # Used before to prevent CSRF attack but PKCE now provides that. Now you can store application specific state.
  code_challenge= XXXXXXXXXXXXX&   # This is the hash of the code verifier
  code_challenge_method=S256       # Hash method used to hash the code verifier

After the user gives her consent (through the consent screen), the authorization server uses the redirect URI, includes the authorization code, and sends it back to the application as follows.

1
2
3
https://example-app.com/redirect?
  code=AUTH_CODE_HERE&
  state=XXXXXX

The application then exchanges the authorization code for the access token by typically making an HTTPS POST request through the back channel to the authorization server’s token endpoint.

1
2
3
4
5
6
7
POST https://authorization-server.com/token
  grant_type=authorization_code&
  code=AUTH_CODE_HERE&
  redirect_uri=REDIRECT_URI&
  code_verifier=VERIFIER_STRING&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

After the exchange request, the authorization server responds with the access token in JSON format as follows.

1
2
3
4
5
6
7
{
  "token_type": "Bearer",
  "acess_token": "XXXXXXXXXXXXX",
  "expires_in": 3600,
  "scope": "photos",
  "refresh_token": "XXXXXXXXXX"
}

Client credentials grant flow-Machine to machine

No user or browser is involved in this flow. The application takes its own credentials and exchanges them for an access token, typically to access its own resources. This is the preferred grant flow if you host your application in AWS and use AWS Cognito as the authorization server.

This flow simplifies API development as there is no need to validate client credentials in addition to the access token.

The following is a single request from the application to the OAuth server to access the application’s own API.

1
2
3
4
5
POST https://authorization-server.com/token
  grant_type=client_credentials&
  scope=contacts&                  #optional and also depends on AuthServer
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

OpenID Connect

OpenID Connect is another industry standard that provides user identity and authentication on top of the OAuth 2.0 framework. OpenID is maintained by the OpenID Foundation. You can use OpenID Connect to establish a login session for your applications, and later, you can use OAuth 2.0 to authorize the user for resources. OpenID Connect can be easily integrated into the OAuth 2.0 code flow.

The key addition that OpenID Connect brings to the picture is an ID Token. This ID Token is how the authorization server communicates information about the user who has just logged into the application. The ID token is represented as a JSON Web Token (JWT).

ID token has three main sections as in every JWT. A header, a payload and the signature. User related information and claims are included in the payload.

Using authorization flow described above just add openid to the scope section:

1
2
3
4
5
6
7
8
https://authorization-server.com/auth?
	response_type=code&
	client_id=CLIENT_ID&
	redirect_uri=REDIRECT_URI&
	scope=photos+openid&
	state=XXXXXX&
	code_challenge= XXXXXXXXXXXXX&
	code_challenge_method=S256

If you request an ID token throughout the authorization flow using the scope, the ID token you receive will contain very little information, primarily metadata about the token, such as the expiration date. It will only include the subject or the user ID of the user. If you require more information, you will need to add additional scope parameters.

OpenID defines the following scopes:

  • openid
  • profile
  • email
  • address
  • phone

You can request OpenID scope as follows.

1
2
3
4
5
6
7
8
https://authorization-server.com/auth?
	response_type=id_token&
	client_id=CLIENT_ID&
	redirect_uri=REDIRECT_URI&
	scope=photos&
	state=XXXXXX&
	code_challenge= XXXXXXXXXXXXX&
	code_challenge_method=S256 

In contrast to authorization flow OpenID token will be sent through redirect URI as follows

1
https://example.com/redirect#id_token=eyJraWQiOixxxxxx&state=xxxxx

OpenID token validation

OpenID token validation is crucial. Unless you are using alternative methods to obtain user information, the ID token is the sole source of user information for applications to authenticate users.

The first step is to validate the JSON Web Token (JWT) by integrating the signing key into the JWT signature validation library.

You should locate the key that signed the token, which can be found in either the header of the OpenID token or hardcoded in the OpenID server documentation. It is best practice to double-check the signing algorithm and ensure that it matches the one you are expecting.

The following is the suggestions for the validation of token claims.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
	"sub": "xxxxxxxxxx",                            # After all checks now you can use subject value to learn the user id of the user.
	"name" "userName userSurname",
	"locale": "en-US",
	"email": "user@mail.com",
	"ver": 1,
	"iss": "https://auth-server.com/oauth2/default", # 1-Check if the id token is coming from the source, you expect it should come
	"aud": "xxxxxxxxxx",                             # 2-Audience claim is the next one. This is application's client id.
	"iat": 1692340544,                               # 3.1-Check if these time stamps are within the bound of current time. 
	"exp": 1692440544,                               # 3.2-Check if these time stamps are within the bound of current time. 
	"jti": "ID.VymjFtpxxxxxxxx",
	"amr": [
	  "pwd"                                          # Optional: For more security you can decide that your user should use multifactor authentication and you can check here if it is the case or not.
	],
	"idp": "xxxxxxxxxxxxxxx",
	"nonce": "xxxxxx",                               # 4-If you're using a front channel flow, your request for an ID Token must contain a nonce value. Check this value if it matches with the one you included in the request.
	"preferred_username": "user@mail.com",
	"given_name": "userName",
	"family_name": "surName",
	"zoneinfo": "Europe/Luxembourg",
	"updated_at": 1692340644,
	"email_verified": true,
	"auth_time": 1692340644,                         # Optional: This is the last time the user logged into the auth server. You check this time to ensure that the last login is not too old.
	"c_hash": "xxxxxxxxxxxxx"
}

Challenges of OAuth 2.0

RFC-6819 outlines a threat model and security considerations for OAuth 2.0 protocol implementations. There is also an ongoing effort to define security best practices.

The primary challenge of OAuth 2.0 is the use of bearer tokens, which can potentially be accessed by other apps if not properly protected. The authorization server must rigorously control application identity, and applications should safeguard their tokens with secure storage solutions.

The most significant vulnerability in the protocol is phishing attacks. Attackers can create web pages that mimic a service’s authorization page, including username and password fields. Through DNS spoofing techniques, they can embed this page within the authorization flow. This threat is especially severe with native and mobile applications, as users cannot see the browser bar to verify the authenticity of the consent screen.

Another threat in the protocol is the redirect URL manipulation. By using a legitimate client ID, an attacker can create an authorization URL with a redirect URL under their control. If the authorization server lacks redirect URL validation and the attacker opts for the token response type, the user may unknowingly return to the attacker’s application with the access token in the URL. For public clients, if the attacker intercepts the authorization code, they can also convert it into an access token.

Various threats challenge the OAuth 2.0 protocol, with many outlined in RFC-6819. In this section, I have discussed only the three primary and commonly encountered attack vectors against the OAuth 2.0 protocol. There are also ongoing efforts which are focused on ensuring the protocol’s secure utilization with an active community behind.

AWS Cognito

You can use the following three OAuth 2.0 grant types using AWS Cognito federation endpoint.

Authorization code grant - This is the authorization code flow explained above. The following is the sample request for this grant in Cognito.

1
2
3
4
5
6
7
GET https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/authorize?
    response_type=code&
    client_id=ad398u21ijw3s9w3939&
    redirect_uri=https://YOUR_APP/redirect_uri&
    state=STATE&
    scope=openid+profile+aws.cognito.signin.user.admin
                    

Implicit grant - In contrast to authorization code grant Access token and ID Token is sent with the call back URL. The request sample as follows.

1
2
3
4
5
6
GET https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/authorize?
    response_type=token&
    client_id=ad398u21ijw3s9w3939&
    redirect_uri=https://YOUR_APP/redirect_uri&
    state=STATE&
    scope=aws.cognito.signin.user.admin                

Client credentials - It is the machine-to-machine code flow explained above. The request sample as follows.

1
2
3
4
5
6
POST https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/token >
    Content-Type='application/x-www-form-urlencoded'&
    Authorization=Basic ZGpjOTh1M2ppZWRtaTI4M2V1OTI4OmFiY2RlZjAxMjM0NTY3ODkw                        
    grant_type=client_credentials&
    client_id=1example23456789&
    scope=resourceServerIdentifier1/scope1 resourceServerIdentifier2/scope2                

With AWS Cognito user pool and using OAuth 2.0 protocol you can do the following

Conclusion

In this article, we have delved into the details of OAuth 2.0 and its significant departure from its predecessor, OAuth 1.0. We’ve highlighted how OAuth 2.0 introduces distinct roles, simplified token handling with Bearer Tokens, and the use of multiple grant types to cater to various application scenarios.

Furthermore, we explored the challenges that OAuth 2.0 implementations may encounter, such as the potential vulnerabilities to phishing attacks and redirect URL manipulation. These security considerations underscore the importance of vigilant implementation to ensure the integrity and safety of OAuth 2.0 in real-world applications.

To put the theory into practice, we examined the utilization of AWS Cognito within the OAuth 2.0 framework. AWS Cognito offers flexible support for various OAuth 2.0 grant types, making it a valuable tool for developers seeking secure and scalable user authentication and access control solutions.

References

Now it’s your turn!

Schedule a 1-on-1 with an ARHS Cloud Expert today!