Complete login with code exchange
Process authentication callbacks and handle redirect flows after users authenticate with Scalekit
Once users have successfully verified their identity using their chosen login method, Scalekit will have gathered the necessary user information for your app to complete the login process. However, your app must provide a callback endpoint where Scalekit can exchange an authorization code to return your app the user details.
-
Validate the
Section titled “Validate the state parameter ”stateparameter recommendedBefore exchanging the authorization code, your application must validate the
stateparameter returned by Scalekit. Compare it with the value you stored in the user’s session before redirecting them. This critical step prevents Cross-Site Request Forgery (CSRF) attacks, ensuring the authentication response corresponds to a request initiated by the same user.Validate state in Express.js const { state } = req.query;// Assumes you are using a session middleware like express-sessionconst storedState = req.session.oauthState;delete req.session.oauthState; // State should be used only onceif (!state || state !== storedState) {console.error('Invalid state parameter');return res.redirect('/login?error=invalid_state');}Validate state in Flask from flask import session, request, redirectstate = request.args.get('state')# Retrieve and remove stored state from sessionstored_state = session.pop('oauth_state', None)if not state or state != stored_state:print('Invalid state parameter')return redirect('/login?error=invalid_state')Validate state in Gin stateParam := c.Query("state")// Assumes you are using a session library like gin-contrib/sessionssession := sessions.Default(c)storedState := session.Get("oauth_state")session.Delete("oauth_state") // State should be used only oncesession.Save()if stateParam == "" || stateParam != storedState {log.Println("Invalid state parameter")c.Redirect(http.StatusFound, "/login?error=invalid_state")return}Validate state in Spring // Assumes HttpSession is injected into your controller methodString storedState = (String) session.getAttribute("oauth_state");session.removeAttribute("oauth_state"); // State should be used only onceif (state == null || !state.equals(storedState)) {System.err.println("Invalid state parameter");return new RedirectView("/login?error=invalid_state");} -
Exchange authorization code for tokens
Section titled “Exchange authorization code for tokens”Once the
stateis validated, your app can safely exchange the authorization code for tokens. The Scalekit SDK simplifies this process with theauthenticateWithCodemethod, which handles the secure server-to-server request.Express.js callback handler app.get('/auth/callback', async (req, res) => {const { code, error, error_description, state } = req.query;// Add state validation here (see previous step)11 collapsed lines// Handle errors firstif (error) {console.error('Authentication error:', error);return res.redirect('/login?error=auth_failed');}if (!code) {return res.redirect('/login?error=missing_code');}try {// Exchange code for user dataconst authResult = await scalekit.authenticateWithCode(code,'https://yourapp.com/auth/callback');const { user, accessToken, refreshToken } = authResult;11 collapsed lines// TODO: Store user session (next guide covers this)// req.session.user = user;res.redirect('/dashboard');} catch (error) {console.error('Token exchange failed:', error);res.redirect('/login?error=exchange_failed');}});Flask callback handler @app.route('/auth/callback')def auth_callback():code = request.args.get('code')error = request.args.get('error')9 collapsed linesstate = request.args.get('state')# TODO: Add state validation here (see previous step)# Handle errors firstif error:print(f'Authentication error: {error}')return redirect('/login?error=auth_failed')if not code:return redirect('/login?error=missing_code')try:# Exchange code for user dataoptions = CodeAuthenticationOptions()auth_result = scalekit.authenticate_with_code(code,'https://yourapp.com/auth/callback',options)user = auth_result.user# access_token = auth_result.access_token# refresh_token = auth_result.refresh_token6 collapsed lines# TODO: Store user session (next guide covers this)# session['user'] = userreturn redirect('/dashboard')except Exception as e:print(f'Token exchange failed: {e}')return redirect('/login?error=exchange_failed')Gin callback handler func authCallbackHandler(c *gin.Context) {code := c.Query("code")errorParam := c.Query("error")13 collapsed linesstateParam := c.Query("state")// TODO: Add state validation here (see previous step)// Handle errors firstif errorParam != "" {log.Printf("Authentication error: %s", errorParam)c.Redirect(http.StatusFound, "/login?error=auth_failed")return}if code == "" {c.Redirect(http.StatusFound, "/login?error=missing_code")return}// Exchange code for user dataoptions := scalekit.AuthenticationOptions{}authResult, err := scalekitClient.AuthenticateWithCode(code,7 collapsed lines"https://yourapp.com/auth/callback",options,)if err != nil {log.Printf("Token exchange failed: %v", err)c.Redirect(http.StatusFound, "/login?error=exchange_failed")return}user := authResult.User// accessToken := authResult.AccessToken// refreshToken := authResult.RefreshToken// TODO: Store user session (next guide covers this)// session.Set("user", user)c.Redirect(http.StatusFound, "/dashboard")}Spring callback handler @GetMapping("/auth/callback")public Object authCallback(@RequestParam(required = false) String code,@RequestParam(required = false) String error,@RequestParam(required = false) String state,10 collapsed linesHttpSession session) {// TODO: Add state validation here (see previous step)// Handle errors firstif (error != null) {System.err.println("Authentication error: " + error);return new RedirectView("/login?error=auth_failed");}if (code == null) {return new RedirectView("/login?error=missing_code");}try {// Exchange code for user dataAuthenticationOptions options = new AuthenticationOptions();AuthenticationResponse authResult = scalekit.authentication().authenticateWithCode(code, "https://yourapp.com/auth/callback", options);var user = authResult.getIdTokenClaims();// String accessToken = authResult.getAccessToken();// String refreshToken = authResult.getRefreshToken();6 collapsed lines// TODO: Store user session (next guide covers this)// session.setAttribute("user", user);return new RedirectView("/dashboard");} catch (Exception e) {System.err.println("Token exchange failed: " + e.getMessage());return new RedirectView("/login?error=exchange_failed");}}The authorization
codecan be redeemed only once and expires in approx ~10 minutes. Reuse or replay attempts typically return errors likeinvalid_grant. If this occurs, start a new login flow to obtain a freshcodeandstate.The
authResultobject returned contains:{user: {email: "john.doe@example.com",emailVerified: true,givenName: "John",name: "John Doe",id: "usr_74599896446906854"},idToken: "eyJhbGciO..", // Decode for full user detailsaccessToken: "eyJhbGciOi..",refreshToken: "rt_8f7d6e5c4b3a2d1e0f9g8h7i6j..",expiresIn: 299 // in seconds}Key Description userCommon user details with email, name, and verification status idTokenJWT containing verified full user identity claims accessTokenShort-lived token that determines current access refreshTokenLong-lived token to obtain new access tokens -
Decoding token claims
Section titled “Decoding token claims”The
idTokenandaccessTokenare JSON Web Tokens (JWT) that contain user claims. These tokens can be decoded to retrieve comprehensive user and access information.Decode ID token // Use a library like 'jsonwebtoken'const jwt = require('jsonwebtoken');// The idToken from the authResult objectconst { idToken } = authResult;// Decode the token without verifying its signatureconst decoded = jwt.decode(idToken);console.log('Decoded claims:', decoded);Decode ID token # Use a library like 'PyJWT'import jwt# The id_token from the auth_result objectid_token = auth_result.id_token# Decode the token without verifying its signaturedecoded = jwt.decode(id_token, options={"verify_signature": False})print(f'Decoded claims: {decoded}')Decode ID token // Use a library like 'github.com/golang-jwt/jwt/v5'import ("fmt""github.com/golang-jwt/jwt/v5")// The IdToken from the authResult objectidToken := authResult.IdTokentoken, _, err := new(jwt.Parser).ParseUnverified(idToken, jwt.MapClaims{})if err != nil {fmt.Printf("Error parsing token: %v\n", err)return}if claims, ok := token.Claims.(jwt.MapClaims); ok {fmt.Printf("Decoded claims: %+v\n", claims)}Decode ID token // Use a library like 'com.auth0:java-jwt'import com.auth0.jwt.JWT;import com.auth0.jwt.interfaces.DecodedJWT;import com.auth0.jwt.interfaces.Claim;import com.auth0.jwt.exceptions.JWTDecodeException;import java.util.Map;try {// The idToken from the authResult objectString idToken = authResult.getIdToken();// Decode the token without verifying its signatureDecodedJWT decodedJwt = JWT.decode(idToken);Map<String, Claim> claims = decodedJwt.getClaims();System.out.println("Decoded claims: " + claims);} catch (JWTDecodeException exception){// Invalid tokenSystem.err.println("Failed to decode ID token: " + exception.getMessage());}The decoded token claims contain:
ID token decoded {"at_hash": "ec_jU2ZKpFelCKLTRWiRsg", // Access token hash for validation"aud": ["skc_58327482062864390" // Audience (your client ID)],"azp": "skc_58327482062864390", // Authorized party (your client ID)"c_hash": "6wMreK9kWQQY6O5R0CiiYg", // Authorization code hash"client_id": "skc_58327482062864390", // Your application's client ID"email": "john.doe@example.com", // User's email address"email_verified": true, // Whether the user's email is verified"exp": 1742975822, // Expiration time (Unix timestamp)"family_name": "Doe", // User's last name"given_name": "John", // User's first name"iat": 1742974022, // Issued at time (Unix timestamp)"iss": "https://scalekit-z44iroqaaada-dev.scalekit.cloud", // Issuer (Scalekit environment URL)"name": "John Doe", // User's full name"oid": "org_59615193906282635", // Organization ID"sid": "ses_65274187031249433", // Session ID"sub": "usr_63261014140912135" // Subject (user's unique ID)}Decoded access token {"aud": ["prd_skc_7848964512134X699" // Audience (API or resource server)],"client_id": "prd_skc_7848964512134X699", // Your application's client ID"exp": 1758265247, // Expiration time (Unix timestamp)"iat": 1758264947, // Issued at time (Unix timestamp)"iss": "https://login.devramp.ai", // Issuer (Scalekit environment URL)"jti": "tkn_90928731115292X63", // JWT ID (unique token identifier)"nbf": 1758264947, // Not before time (Unix timestamp)"oid": "org_89678001X21929734", // Organization ID"permissions": [ // Scopes or permissions granted"workspace_data:write","workspace_data:read"],"roles": [ // User roles within the organization"admin"],"sid": "ses_90928729571723X24", // Session ID"sub": "usr_8967800122X995270", // Subject (user's unique ID)// External identifiers if updated on Scalekit"xoid": "ext_org_123", // External organization ID"xuid": "ext_usr_456" // External user ID}
An IdToken contains comprehensive profile information about the user. You can save this in your database for app use cases, using your own identifier. Now, let’s utilize access and refresh tokens to manage user access and maintain active sessions.