File length: 17641 # Authentication - Machine to Machine - Client credentials authentication Source: https://supertokens.com/docs/authentication/m2m/client-credentials ## Overview In the **Client Credentials Flow** the authentication sequence works in the following way: ## `Service A` uses credentials to get an **OAuth2 Access Token** ## [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) returns the **OAuth2 Access Token** ## `Service A` uses the **OAuth2 Access Token** to communicate with `Service B` ## `Service B` validates the **OAuth2 Access Token** ## If the token is valid `Service B` returns the requested resource Machine to Machine Authentication Before going into the actual instructions, start by imagining a real life example that you can reference along the way. This makes it easier to understand what is happening. We are going to configure authentication for the following setup: - A **Calendar Service** that exposes these actions: `event.view`, `event.create`, `event.update` and `event.delete` - A **File Service** that exposes these actions: `file.view`, `file.create`, `file.update` and `file.delete` - A **Task Service** that interacts with the **Calendar Service** and the **File Service** in the process of scheduling a task The aim is to allow the **Task Service** to perform an authenticated action on the **Calendar Service**. Proceed to the actual steps. ## Before you start These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page, please follow the tutorial and return here once you're done. ## Steps ### 1. Enable the OAuth2 features from the Dashboard You first have to enable the **OAuth2** features from the **SuperTokens.com Dashboard**. 1. Open the **SuperTokens.com Dashboard** 2. Click on the **Enabled Paid Features** button 3. Click on **Managed Service** 4. Check the **Unified Login / M2M** option 5. Click *Save* You should be able to use the OAuth2 recipes in your applications. ### 2. Create the OAuth2 Clients For each of your **`microservices`** you need to create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). This can occur by directly calling the **SuperTokens Core** API. ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "grantTypes": ["client_credentials"], "scope": " ", "audience": [""], } ' ``` ```tsx const BASE_URL = ''; const API_KEY = '^{coreInfo.key}'; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: 'POST', headers: { 'api-key': API_KEY, 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ clientName: "", grantTypes: ["client_credentials"], scope: " ", audience: [""], }) }; fetch(url, options) .then(response => response.json()) .then(json => console.log(json)) .catch(err => console.error(err)); ``` ```go BASE_URL = "" API_KEY = "^{coreInfo.key}" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "grantTypes": ["client_credentials"], "scope": "custom_scope_1> ", "audience": [""], } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` :::info Custom Example To create a client for the **Task Service**, use the following attributes: ```json { "clientName": "Task Service", "grantTypes": ["client_credentials"], "scope": "event.view event.create event.edit event.delete file.view file.create file.edit file.delete", "audience": ["event", "file"] } ``` This allows the **Task Service** to perform all types of actions against both of the other services as long as it has a valid **OAuth2 Access Token**. ::: :::caution You have to save the create response because this is not persisted internally for security reasons. The information is necessary for the next steps. ::: ### 3. Set Up your Authorization Service In your [**Authorization Server**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend, initialize the **OAuth2Provider** recipe. Update the `supertokens.init` call to include the new recipe. ```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ OAuth2Provider.init(), ] }); ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. You can use the [legacy method](/docs/microservice_auth/legacy/implementation-guide) to authenticate microservices based on your language. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init() ], ) ``` ### 4. Generate access tokens You can directly call the [**Authorization Server**](/docs/authentication/unified-login/oauth2-basics#authorization-server) to generate Access Tokens. Check the following code snippet to see how you can do that: ```bash curl -X POST /oauth/token \ -H "Content-Type: application/json" \ -d '{ "client_id": "", "client_secret": "", "grant_type": "client_credentials", "scope": [""], "audience": "" }' ``` You should limit the scopes that you are requesting to the ones necessary to perform the desired action. :::info Custom Example If the **Task Service** wants to create an event on the **Calendar Service**, a token with the following attributes needs generation: ```json { "client_id": "", "client_secret": "", "grant_type": "client_credentials", "scope": ["event.create"], "audience": "event" } ``` ::: The **Authorization Server** returns a response that looks like this: ```json { "access_token": "", "expires_in": 3600 } ``` Save the `access_token` in memory for use in the next step. The `expires_in` field indicates how long the token is valid for. Each service that you communicate with needs its own token. With an **OAuth2 Access Token**, it can facilitate communication with the other services. Keep in mind to generate a new one when it expires. ### 5. Verify an OAuth2 Access Token To check the validity of a token, use a generic **JWT** verification library. Besides the standard **OAuth2** token claims, the implementation includes an additional one called `stt`. This stands for `SuperTokens Token Type`. It ensures that the validation occurs for the correct token type: - `0` represents a **SuperTokens Session Access Token** - `1` represents an **OAuth2 Access Token** - `2` represents an **OAuth2 ID Token**. For NodeJS you can use [`jose`](https://github.com/panva/jose) to verify the token. ```tsx const JWKS = jose.createRemoteJWKSet(new URL('/jwt/jwks.json')) async function validateClientCredentialsToken(jwt: string) { const requiredScope = ""; const audience = ''; try { const { payload } = await jose.jwtVerify(jwt, JWKS, { audience, requiredClaims: ['stt', 'scp'], }); if(payload.stt !== 1) return false; const scopes = payload.scp as string[]; return scopes.includes(requiredScope); } catch (err) { return false; } } ``` You can use the [`jwx`](https://github.com/lestrrat-go/jwx) library to verify the token. ```go return true } } return false; } ``` You can use the [PyJWT](https://github.com/jpadilla/pyjwt) library to verify the token. ```python from typing import Optional, List use Firebase\JWT\JWT; use Firebase\JWT\Key; function validateToken($jwt) { $apiDomain = ""; $apiBasePath = ""; $jwksUrl = $apiDomain . $apiBasePath . '/jwt/jwks.json'; $requiredScope = ""; $audience = ""; $jwks = json_decode(file_get_contents($jwksUrl), true); try { $decoded = JWT::decode($jwt, JWK::parseKeySet($jwks), 'RS256')); if ($decoded->aud !== $audience) { return false; } if ($decoded->sst !== 1) { return false; } return in_array($requiredScope, $decoded->scp); } catch (Exception $e) { return false; } } ``` You can use the [Auth0 JWT](https://github.com/auth0/java-jwt) library to verify the token. ```java public class JWTVerifier { private static final String JWKS_URL = "jwt/jwks.json"; private static final String AUDIENCE = ""; private static Map fetchJWKS() throws Exception { URL url = new URL(JWKS_URL); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); InputStream responseStream = connection.getInputStream(); Scanner scanner = new Scanner(responseStream, StandardCharsets.UTF_8.name()); String responseBody = scanner.useDelimiter("\\A").next(); scanner.close(); return JWT.decode(responseBody).getHeader(); } public static boolean validateToken(String token) { try { Map jwks = fetchJWKS(); Algorithm algorithm = Algorithm.RSA256(jwks.get("x5c"), null); JWTVerifier verifier = JWT.require(algorithm) .withAudience(AUDIENCE) .build(); DecodedJWT jwt = verifier.verify(token); if(jwt.getClaim("sst").asInt() != 1) { return false; } List scopes = jwt.getClaim("scp").asList(); return scopes.contains(requiredScope); } catch (Exception e) { return false; } } } ``` You can use the [IdentityModel](https://github.com/IdentityModel/IdentityModel) library to verify the token. ```csharp using System; using System.Linq; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json.Linq; class ClientCredentialsTokenValidator { static async Task ValidateToken(string jwtStr) { string apiDomain = ""; string apiBasePath = ""; string audience = ""; string requiredScope = ""; HttpClient client = new HttpClient(); var response = await client.GetStringAsync($"//jwt/jwks.json"); var jwks = new JsonWebKeySet(response); var tokenHandler = new JwtSecurityTokenHandler(); var validationParameters = new TokenValidationParameters { ValidAudience = audience, IssuerSigningKeys = jwks.Keys }; try { SecurityToken validatedToken; var principal = tokenHandler.ValidateToken(jwtStr, validationParameters, out validatedToken); var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value); if (!claims.ContainsKey("stt") || claims["stt"] != "1") { return false; } var scopes = claims["scp"].Split(" "); if (!scopes.Contains(requiredScope)) { return false; } return true; } catch (Exception) { return false; } } } ``` :::info Custom Example If the **Task Service** uses the previously generated token to create a calendar event, the **Calendar Service** needs to check the following: - Set the `stt` claim to `1` - The `scp` claim contains `event.create` - Set the `aud` claim to `event` ::: #### Handle both SuperTokens session tokens and OAuth2 access tokens If you are using your **Authorization Service** also as a **Resource Server**, account for this in the way you verify the sessions. This is necessary because two types of tokens are in use: - **SuperTokens Session Access Token**: Used during the login/logout flows. - **OAuth2 Access Token**: Used to access protected resources and perform actions that need authorization. Hence, a way to distinguish between these two and prevent errors is necessary. ```tsx async function verifySession(req: Request, res: Response, next: NextFunction) { let session = undefined; try { session = await Session.getSession(req, res, { sessionRequired: false }); } catch (err) { if ( !Session.Error.isErrorFromSuperTokens(err) || err.type !== Session.Error.TRY_REFRESH_TOKEN ) { return next(err); } } // In this case we are dealing with a SuperTokens Session that has been validated if (session !== undefined) { return next(); } // The OAuth2 Access Token needs to be manually extracted and validated let jwt: string | undefined = undefined; if (req.headers["authorization"]) { jwt = req.headers["authorization"].split("Bearer ")[1]; } if (jwt === undefined) { return next(new Error("No JWT found in the request")); } try { await validateToken(jwt); return next(); } catch (err) { return next(err); } } const JWKS = jose.createRemoteJWKSet( new URL("jwt/jwks.json"), ); // This is a basic example on how to validate an OAuth2 Token // Use the previous example to extend it async function validateToken(jwt: string) { const { payload } = await jose.jwtVerify(jwt, JWKS, { requiredClaims: ["stt", "scp", "sub"], }); if (payload.stt !== 1) throw new Error("Invalid token"); // If the Authorizaton Server will handle different types of Authorization Flows // You can differentiate between the different types of tokens by checking the `sessionHandle` claim const sessionHandle = payload['sessionHandle'] as string | undefined; if(sessionHandle === undefined) { // We are dealing with a Client Credentials Token // You can perform microservice authentication checks here } else { // Here we are validating tokens that have been generated in the Authorization Code Flow } } // You can then use the function as a middleware for a protected route const app = express(); app.get("/protected", verifySession, async (req, res) => { // Custom logic }); ``` ```python from supertokens_python.recipe.session.syncio import get_session from supertokens_python.recipe.session.exceptions import SuperTokensSessionError, TryRefreshTokenError from fastapi.requests import Request from typing import List, Optional return True def validate_token(token: str, required_scope: str) -> bool: api_domain = "" api_base_path = "/auth" client_id = "" jwks_url = f"{api_domain}{api_base_path}jwt/jwks.json" jwks_client = PyJWKClient(jwks_url) try: signing_key = jwks_client.get_signing_key_from_jwt(token) decoded = jwt.decode( token, signing_key.key, algorithms=['RS256'], options={"require": ["stt", "client_id", "scp"]} ) stt: Optional[int] = decoded.get('stt') if stt != 1: return False token_client_id: Optional[str] = decoded.get('client_id', None) if client_id != token_client_id: return False scopes: List[str] = decoded.get('scp', []) if required_scope not in scopes: return False return True except Exception: return False # ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. :::