Introduction
Social logins and enterprise connections are popular because they are an extremely effective way to register users lowering sign up frictions (often up to 20% higher conversion or more) as well as a way to connect internal enterprise directories to a SaaS application.
Besides SAML, which is largely used for enterprise directories, the standard for social login is a protocol called OpenID Connect (OIDC). This is an identity layer built on top of the OAuth 2.0 framework. It allows third-party applications to verify the identity of the end-user and to obtain basic user profile information. OIDC uses JSON web tokens (JWTs), which are obtained through flows defined in the OAuth 2.0 specifications.
Unfortunately OAuth 2.0 is a complex protocol and several implementations suffer from security issues.
Just recently Salt Security published a series of blog posts on OAuth 2.0 implementation bugs found in sites such as Booking.com, Kayak, Grammarly, and many more.
This is in addition to the vulnerability reported to Descope that can lead to privilege escalation when using Entra/Azure AD for SSO and the one reported by Truffle Security that could lead to a similar outcome with Google.
In this blog post we summarize the best practices in implementing OAuth 2.0 securely followed by a deeper dive on the risk posed by each.
OAuth 2.0 and OIDC authorization at a high level
Feel free to skip this section if you are already familiar with OAuth 2.0 and OIDC
It would take too long to describe all the possible OAuth 2.0 flows and they are generally well documented online. To summarize in a picture:
At a high-level, here is some contextual information useful for the rest of the article:
-
Roles:
- Resource Owner: The user who authorizes an application to access their account.
- Resource Server: The server hosting the user data.
- Client: The application wanting to access the user’s account.
- Authorization Server: The server that authenticates the resource owner and issues access tokens to the client.
-
Flow:
- The client requests authorization from the resource owner to access their resources.
- If the resource owner grants authorization, the client receives an authorization grant, which is a credential representing the resource owner’s authorization.
- The client requests an access token from the authorization server by presenting the authorization grant and authentication.
- If the request is valid, the authorization server issues an access token to the client.
- The client uses the access token to access the protected resources hosted by the resource server.
-
Authorization Grant Types: OAuth 2.0 defines several grant types but in this article we’ll touch on the two most commonly used ones:
- Authorization Code: Used with web applications.
- Implicit: Simplified flow, mostly used by mobile or web applications.
-
Access Token:
- This token represents the authorization of a specific application to access specific parts of a user’s data.
- The client must send this token to the resource server in every request.
- Tokens are limited in scope and duration.
As it is often explained publicly, OAuth 2.0 doesn’t provide an authentication flow hence it is generally used in conjuction to OpenID Connect (OIDC). OIDC allows clients to verify the identity of an end-user based on the authentication performed by an authorization server, as well as to obtain basic profile information about the end-user. At a high-level:
-
Roles:
- End-User: The individual whose identity is being verified.
- Client: The application requiring the end-user’s identity, typically a web app, mobile app, or server-side application.
- Authorization Server (OpenID Provider): The server that authenticates the end-user and provides identity tokens to the client.
-
Flow:
- The client requests authorization from the end-user. This is typically done through a user agent (like a web browser) and involves redirecting the user to the authorization server.
- The authorization server authenticates the end-user. This may involve the user logging in with a username and password or other authentication methods.
- Once authenticated, the end-user is asked to grant permission for the client to access their identity information.
- After the end-user grants permission, the authorization server redirects back to the client with an ID Token and, often, an Access Token.
- The ID Token is a JSON Web Token (JWT) that contains claims about the identity of the user. The client will validate this token to ensure it’s authentic.
- The Access Token can be used by the client to access the user’s profile information via a user-info endpoint on the authorization server.
-
Scopes and Claims:
- During the authorization request, the client specifies the scope of the access it is requesting. Common scopes in OpenID Connect include
openid
(required),profile
,email
, etc. - Claims are pieces of information about the user, such as the user’s name, email, and so forth. These are included in the ID Token based on the requested scopes.
- During the authorization request, the client specifies the scope of the access it is requesting. Common scopes in OpenID Connect include
In practice, at a high level, a client is given a client_id
and a client_secret
. The client initiates a request sending those two parameters among others to the authorization server, the authorization server first verifies the validity of the client_id
and client_secret
and then proceeds to verify the user. Once that’s done, the user is redirected to a redirect_uri
passed to the authorization server by the client. From there on, depending on the OAuth 2.0 flow, there might be further exchanges between the client and the authorization server server.
Security best practices
This is the a list of security checks to perform when implementing OIDC:
- Only use authoritative claims
- Enforce exact paths in OAuth 2.0 providers configuration
- Avoid the implicit OAuth 2.0 grant type
- Use PKCE to avoid CSRF and interception attacks
- Verify the token received from a third-party IdP and check that the client id is the intended client id for the application
- Be mindful of which providers you accept on your website
Only use authoritative claims
This issue has presented itself in a number of different flavors across major identity providers including Microsoft Entra, Google and AWS Cognito. For example, Flickr was compromised through a version of this issue.
Microsoft calls the issue “the false identifier anti-pattern”:
The false identifier anti-pattern occurs when an application or service assumes that an attribute other than the subject of an assertion from a federated identity provider is a unique, durable, and trustworthy account identifier during single sign-on.
Microsoft Entra
The issue stems from the fact that in AzureAD/Entra a user without a provisioned mailbox can have any email address set for their Mail (Primary SMTP) attribute. This attribute is not guaranteed to come from a verified email address.
However most clients retrieving the email address claim through OIDC would treat it as a verified/authoritative email address.
For example, an attacker could have an Azure AD account with an email such as satya.nadella@microsoft.com
and the receiving client would treat that as the valid
email address for that user. If Satya Nadella actually had an account in that specific client that could lead to a privilege escalation/unauthorized impersonation where the attacker could impersonate Nadella.
It’s important to note that while this specific issue was about the email address claim in AzureAD, the more general pattern is for clients to always verify which claims are authoritative for a Resource Server and maintain the same “privileges” over them.
In fact, similarly to the Entra issues, researchers at Truffle Security discovered a very similar issue with Google. In particular, it is possible to create a Google account with an existing email. Contrary to the AzureAD case, the ownership of the email is verified. However depending on the implementation of the client this could still lead to two issues:
- Insider Threat: an existing employee can create a shadow account (by leveraging the + sign aliases) in the target client/application. If the application allows for multiple idenfiers to be specified or very long-lived tokens, the insider might be able to maintain access to the shadow account even after the employee has left the company/has been terminated. Such a vulnerability was found by Truffle Security in both Zoom and Slack.
- Privilege escalation/Unauthorized impersonation: Several researchers have shown how somebody can abuse customer support ticketing systems to impersonate other domains using magic links. In such a scenario, an attacker could gain access to a company application by combining the two issues together.
Note: Google has a OAuth 2.0 claim called
email_verified
, it might be possible to create Google accounts without any email verification at all leading to more unauthorized impersonation issues.
AWS Cognito
The last example is another variation on this but easier to follow. A website can use AWS Cognito as an OAuth 2.0 provider leading to a similar issue.
As well described in this blog post on Flickr, by default Cognito allows users to change their user attributes, including the email attribute email
.
In the case of Flickr, the website was using the standard OIDC flow to authenticate a user and they referenced the email
claim without verying the flag in the email_verified
claim. This lead to a scenario where an attacker could takeover any Flickr account with a simple process:
- Create an attacker account on Flickr and authenticate
- Extract the AWS Cognito access token and using the aws cli change the
email
claim to the victim email - Re-login with the attacker account in (1)
Flickr use the email
claim to link the Flickr account to the AWS Cognito user resulting in an arbitrary account takeover.
Enforce exact paths in OAuth 2.0 providers configuration
When a client registers with an Authorization Server generally a redirect_uri
has to be specified, the Authorization Server
will then verify that the URL passed during the OIDC flow matches the redirect_uri
stored for that client.
Unfortunately most Authorization Servers allow wildcards in the path or some even allow no redirect_uri
at all. There’s nothing
the client can do in the latter case but in the former it is crucial to specify exact urls instead of wildcards.
This is because an attacker can potentially trick a victim into being redirected to a different origin than intended and thus potentially stealing the access token.
Avoid the implicit OAuth 2.0 grant type
The implicit OAuth 2.0 grant type was previously recommended for native apps and SPA applications. In this flow, the access token is returned directly as part of the redirect.
Combining this with the previous issue, can lead to a simple way to impersonate the user by stealing the access token when the user is redirected to a
redirect_uri
that is attacker-controlled.
Use Proof Key for Code Exchange (PKCE) to avoid CSRF and interception attacks
Cross-Site Request Forgery (CSRF) is a vulnerability that occurs when an attacker can cause a victim to perform an unintended action on a web resource. In the context of OAuth 2.0, a code interception attack is an attack where the attacker intercepts an authorization code and tries to redeem it for a valid access token.
A technique called Proof Key for Code Exchange (PKCE, pronounced “pixy”) was introduced to reduce the risk of CSRF and code interception. PKCE is an addition to any OAuth 2.0 flows.
In summary, the idea is for the application/client to select a secret, one-time nonce that cannot be intercepted by an attacker as it is never sent to the user browser.
When a flow starts, the client hashes the nonce and sends it in the code_challenge
parameter to the authorization server.
When the user completes the authorization flow and is redirected to the redirect_uri
, the client sends the code_verifier
in addition to the auth code and the state parameters to the authorization server. The authorization server returns a valid access token only if the hash of the code_verified
matches the code_challenge
.
The key idea is that an attacker who intercepts the authorization code from the server is unable to redeem it for an access token, as they are not in possession of the code_verifier
secret.
The authorization server would reject a token request if the attacker tries to inject an authorization code via CSRF.
PKCE also protects against a network attacker because the attacker would need to inject a code that is bound to the same code challenge that was used initially by the client. While the attacker can create codes bound to arbitrary code challenges, the attacker cannot know the code challenge used in any one legitimate session.
Verify the token received from a third-party IdP
If you do need to support the implicit OAuth 2.0 grant type it is key to verify the token received in the callback as the callback could be invoked by an attacker with a valid access token belonging to a different website.
In this scenario, an attacker can intercept the OAuth 2.0 flow and swap the token with a token issued from an attacker-controlled website. Specifically:
- Trick a user into logging-in through OIDC on an attacker-controlled website
- Begin the OIDC flow on the target website pretending to be the user. This way the attacker is able to capture the
state
parameter used by the client and the Authorization Server to identify a “session” - Use the
state
parameter from (2) combined with the access token for the user obtained in (1) to complete the authentication flow on the target client/website.
To address this problem, it is crucial to confirm that the token received was issued for the client client_id
and not for a third party application.
Be mindful of which providers you accept on your website
Duplicated accounts are one of the most vexing issues for both users and companies. On the user side, duplicate accounts lead to frustration and bad experience. On the company side it leads to customer support tickets as well as issues with data analysis.
To address this problem a lot of websites automatically merge accounts if the primary user identifier is the same. In other words, a user who registered with user@example.com
and has account number 123
will be treated as the same person if the primary identifier user@example.com
comes from OIDC, a magic link or any other authentication method.
This is a more general case of the issues we’ve see in (1), specifically an attacker could impersonate a legitimate user if account merging is turned on a website and the website has one of two issues:
- Has a weakness such as trusting a claim that is not authoritative
- Supports a OIDC provider for which an attacker can obtain a token and the website is not implementing countermeasures as indicated in the section above
If you are a SlashID customer
Ultimately mitigating these issues requires the expert review of a security engineer or an application security review. However, using a vendor can make the task less daunting. In particular, if you are a SlashID customer we help you in the following ways:
- We do not support OAuth 2.0 implicit flow
- We enforce PKCE
- For all Identity Providers we support, we return an email claim only if it’s verified
- We only support SSO providers that we have vetted and consider trustworthy
- You can use our SDK to send an email verification link after the user logs in with SSO
Conclusion
Authentication is a typical example of a task that is simple in theory but very hard in practice due to both security and reliability issues. In this blog post we’ve shown a few examples of what could go wrong when implementing OIDC/Social Logins.
If you are interested in implementing social logins and identity securely, get a free account here or reach out to us!