Quickstart
Get up and running with Scalekit’s Full Stack Authentication in under 10 minutes. You’ll build a complete authentication flow with hosted sign-in pages, user session management, and secure logout - all optimized for B2B SaaS applications.
What you’ll build
Section titled “What you’ll build”By the end of this guide, your application will have:
- Hosted authentication pages - Let Scalekit handle sign-in/sign-up UI
- Multi-tenant user management - Automatic workspace separation for B2B apps
- Session management - Secure token handling with automatic refresh
- Multiple auth methods - Email/password, social logins, and enterprise SSO
- Secure logout - Complete session invalidation
How it works
Section titled “How it works”Scalekit handles the complex authentication flow while you focus on your core product:
- User initiates sign-in - Your app redirects to Scalekit’s hosted auth page
- Identity verification - User authenticates via their preferred method
- Secure callback - Scalekit returns user profile and session tokens
- Session creation - Your app establishes a secure user session
- Protected access - User can access your application’s features
Before you begin
Section titled “Before you begin”- Signup for a Scalekit account
- Copy your API credentials from the Scalekit dashboard’s API Config section
- Register your redirect URLs in the Scalekit Dashboard under Redirects section
Let’s get started!
-
Install the Scalekit SDK
Section titled “Install the Scalekit SDK”npm install @scalekit-sdk/nodepip install scalekit-sdk-pythongo get -u github.com/scalekit-inc/scalekit-sdk-go/* Gradle users - add the following to your dependencies in build file */implementation "com.scalekit:scalekit-sdk-java:1.1.3"<!-- Maven users - add the following to your `pom.xml` --><dependency><groupId>com.scalekit</groupId><artifactId>scalekit-sdk-java</artifactId><version>1.1.3</version></dependency>Copy your API credentials from the Scalekit dashboard’s API Config section and set them as environment variables.
Terminal window SCALEKIT_ENVIRONMENT_URL='<YOUR_ENVIRONMENT_URL>'SCALEKIT_CLIENT_ID='<ENVIRONMENT_CLIENT_ID>'SCALEKIT_CLIENT_SECRET='<ENVIRONMENT_CLIENT_SECRET>'Create a new Scalekit client instance after initializing the environment variables.
utils/auth.js import { Scalekit } from '@scalekit-sdk/node';export let scalekit = new Scalekit(process.env.SCALEKIT_ENVIRONMENT_URL,process.env.SCALEKIT_CLIENT_ID,process.env.SCALEKIT_CLIENT_SECRET);utils/auth.py from scalekit import ScalekitClientimport osscalekit = ScalekitClient(os.environ.get('SCALEKIT_ENVIRONMENT_URL'),os.environ.get('SCALEKIT_CLIENT_ID'),os.environ.get('SCALEKIT_CLIENT_SECRET'))utils/auth.go package mainimport ("os""github.com/scalekit-inc/scalekit-sdk-go")var scalekit, err = scalekit.NewScalekitClient(os.Getenv("SCALEKIT_ENVIRONMENT_URL"),os.Getenv("SCALEKIT_CLIENT_ID"),os.Getenv("SCALEKIT_CLIENT_SECRET"),)if err != nil {panic(err)}utils/Auth.java import com.scalekit.ScalekitClient;import com.scalekit.Environment;public class Auth {public static ScalekitClient scalekit;static {Environment.configure(System.getenv("SCALEKIT_ENVIRONMENT_URL"),System.getenv("SCALEKIT_CLIENT_ID"),System.getenv("SCALEKIT_CLIENT_SECRET"));scalekit = new ScalekitClient(System.getenv("SCALEKIT_ENVIRONMENT_URL"),System.getenv("SCALEKIT_CLIENT_ID"),System.getenv("SCALEKIT_CLIENT_SECRET"));}} -
Redirect users to authentication page
Section titled “Redirect users to authentication page”Generate the authorization URL by passing a registered callback URL and scopes to the Scalekit SDK.
Express.js const redirectUri = '<http://localhost:3000/api/callback>';const options = {scopes: ['openid', 'profile', 'email', 'offline_access']};const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);res.redirect(authorizationUrl);Flask from scalekit import AuthorizationUrlOptionsredirect_uri = 'http://localhost:3000/api/callback'options = AuthorizationUrlOptions(scopes=['openid', 'profile', 'email', 'offline_access'])authorization_url = scalekit.get_authorization_url(redirect_uri, options)# For web frameworks like Flask/Django:# return redirect(authorization_url)Gin redirectUri := "http://localhost:3000/api/callback"options := scalekit.AuthorizationUrlOptions{Scopes: []string{"openid", "profile", "email", "offline_access"}}authorizationUrl, err := scalekit.GetAuthorizationUrl(redirectUri, options)if err != nil {// handle error appropriatelypanic(err)}// For web frameworks like Gin:// c.Redirect(http.StatusFound, authorizationUrl.String())Spring import com.scalekit.internal.http.AuthorizationUrlOptions;import java.net.URL;import java.util.Arrays;String redirectUri = "http://localhost:3000/api/callback";AuthorizationUrlOptions options = new AuthorizationUrlOptions();options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options);This will redirect the user to Scalekit’s managed sign-in page.
-
Handle Authentication Callback
Section titled “Handle Authentication Callback”After users authenticate, Scalekit redirects them back to your registered callback URL with an authorization code. Exchange this code to get the user’s profile and session tokens:
import scalekit from '@/utils/auth.js'const redirectUri = '<http://localhost:3000/api/callback>';// Get the authorization code from the scalekit initiated callbackapp.get('/api/callback', async (req, res) => {const { code, error, error_description } = req.query;if (error) {return res.status(401).json({ error, error_description });}try {// Exchange the authorization code for a user profileconst authResult = await scalekit.authenticateWithCode(code, redirectUri);const { user } = authResult;// "user" object contains the user's profile information// Next step: Create a session and log in the user5 collapsed linesres.redirect('/dashboard/profile');} catch (err) {console.error('Error exchanging code:', err);res.status(500).json({ error: 'Failed to authenticate user' });}});6 collapsed linesfrom flask import Flask, request, redirect, jsonifyfrom scalekit import ScalekitClient, CodeAuthenticationOptionsapp = Flask(__name__)# scalekit imported from your auth utilsredirect_uri = 'http://localhost:3000/api/callback'@app.route('/api/callback')def callback():code = request.args.get('code')error = request.args.get('error')error_description = request.args.get('error_description')if error:return jsonify({'error': error, 'error_description': error_description}), 401try:# Exchange the authorization code for a user profileoptions = CodeAuthenticationOptions()auth_result = scalekit.authenticate_with_code(code, redirect_uri, options)user = auth_result.user# "user" object contains the user's profile information# Next step: Create a session and log in the user4 collapsed linesreturn redirect('/dashboard/profile')except Exception as err:print(f'Error exchanging code: {err}')return jsonify({'error': 'Failed to authenticate user'}), 5009 collapsed linespackage mainimport ("log""net/http""os""github.com/gin-gonic/gin""github.com/scalekit-inc/scalekit-sdk-go")// Create Scalekit client instancevar scalekitClient = scalekit.NewScalekitClient(os.Getenv("SCALEKIT_ENVIRONMENT_URL"),os.Getenv("SCALEKIT_CLIENT_ID"),os.Getenv("SCALEKIT_CLIENT_SECRET"),)const redirectUri = "http://localhost:3000/api/callback"func callbackHandler(c *gin.Context) {code := c.Query("code")errorParam := c.Query("error")errorDescription := c.Query("error_description")if errorParam != "" {c.JSON(http.StatusUnauthorized, gin.H{"error": errorParam,"error_description": errorDescription,})return}// Exchange the authorization code for a user profileoptions := scalekit.AuthenticationOptions{}authResult, err := scalekitClient.AuthenticateWithCode(code, redirectUri, options,)if err != nil {log.Printf("Error exchanging code: %v", err)c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to authenticate user",})return}user := authResult.User// "user" object contains the user's profile information// Next step: Create a session and log in the userc.Redirect(http.StatusFound, "/dashboard/profile")}10 collapsed linesimport com.scalekit.ScalekitClient;import com.scalekit.internal.http.AuthenticationOptions;import com.scalekit.internal.http.AuthenticationResponse;import org.springframework.web.bind.annotation.*;import org.springframework.web.servlet.view.RedirectView;import org.springframework.http.ResponseEntity;import org.springframework.http.HttpStatus;import java.util.HashMap;import java.util.Map;@RestControllerpublic class CallbackController {private final String redirectUri = "http://localhost:3000/api/callback";@GetMapping("/api/callback")public Object callback(@RequestParam(required = false) String code,@RequestParam(required = false) String error,@RequestParam(name = "error_description", required = false) String errorDescription) {if (error != null) {Map<String, String> errorResponse = new HashMap<>();errorResponse.put("error", error);errorResponse.put("error_description", errorDescription);return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);}try {// Exchange the authorization code for a user profileAuthenticationOptions options = new AuthenticationOptions();AuthenticationResponse authResult = scalekit.authentication().authenticateWithCode(code, redirectUri, options);var user = authResult.getUser();// "user" object contains the user's profile information// Next step: Create a session and log in the userreturn new RedirectView("/dashboard/profile");8 collapsed lines} catch (Exception err) {System.err.println("Error exchanging code: " + err.getMessage());Map<String, String> errorResponse = new HashMap<>();errorResponse.put("error", "Failed to authenticate user");return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);}}}The
authenticateWithCode
method returns an object containing the user’s profile information (user
object) and session tokens includingidToken
,accessToken
andrefreshToken
.{user: {email: "john.doe@example.com",emailVerified: true,givenName: "John",name: "John Doe",id: "usr_74599896446906854"},idToken: "eyJhbGciO..",accessToken: "eyJhbGciOi..",refreshToken: "rt_8f7d6e5c4b3a2d1e0f9g8h7i6j..",expiresIn: 299}{"at_hash": "ec_jU2ZKpFelCKLTRWiRsg","aud": ["skc_58327482062864390"],"azp": "skc_58327482062864390","c_hash": "6wMreK9kWQQY6O5R0CiiYg","client_id": "skc_58327482062864390","email": "john.doe@example.com","email_verified": true,"exp": 1742975822,"family_name": "Doe","given_name": "John","iat": 1742974022,"iss": "https://scalekit-z44iroqaaada-dev.scalekit.cloud","name": "John Doe","oid": "org_59615193906282635","sid": "ses_65274187031249433","sub": "usr_63261014140912135"}You can decode the
idToken
to access user information like email, name, and profile verification status directly from the token claims. -
Create and manage user sessions
Section titled “Create and manage user sessions”Now that you’ve the entire user’s profile details including their email address, verification status, create a secure session by storing the session tokens. Use encrypted HTTP-only cookies for the access token and securely store the refresh token:
import cookieParser from 'cookie-parser';// Set cookie parser middlewareapp.use(cookieParser());// encrypt the accessToken using a secure encryption algorithmconst encryptedAccessToken = encrypt(authResult.accessToken);// setting up accessToken as HTTP-only cookieres.cookie('accessToken', encryptedAccessToken, {maxAge: (authResult.expiresIn - 60) * 1000,httpOnly: true,secure: true,path: '/',sameSite: 'strict'});// Store the refreshToken in a secure placefrom flask import Flask, make_responsefrom cryptography.fernet import Fernetimport os# Cookie parsing is built-in with Flask's request objectapp = Flask(__name__)# encrypt the accessToken using a secure encryption algorithmkey = os.environ.get('ENCRYPTION_KEY') # Store securelycipher_suite = Fernet(key)encrypted_access_token = cipher_suite.encrypt(authResult['accessToken'].encode()).decode()# setting up accessToken as HTTP-only cookieresponse = make_response()response.set_cookie('accessToken',encrypted_access_token,max_age=(authResult['expiresIn'] - 60) * 1000,httponly=True,secure=True,path='/',samesite='Strict')# Store the refreshToken in a secure placeimport ("crypto/aes""crypto/cipher""net/http""os")// encrypt the accessToken using a secure encryption algorithmkey := []byte(os.Getenv("ENCRYPTION_KEY"))block, _ := aes.NewCipher(key)gcm, _ := cipher.NewGCM(block)encryptedAccessToken, _ := encrypt(authResult.AccessToken)// setting up accessToken as HTTP-only cookiecookie := &http.Cookie{Name: "accessToken",Value: encryptedAccessToken,MaxAge: (authResult.ExpiresIn - 60) * 1000,HttpOnly: true,Secure: true,Path: "/",SameSite: http.SameSiteStrictMode,}http.SetCookie(w, cookie)// Store the refreshToken in a secure placeimport javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServletResponse;import java.util.Base64;// encrypt the accessToken using a secure encryption algorithmSecretKey secretKey = getSecretKey(); // Get from secure storageCipher cipher = Cipher.getInstance("AES");cipher.init(Cipher.ENCRYPT_MODE, secretKey);String encryptedAccessToken = Base64.getEncoder().encodeToString(cipher.doFinal(authResult.getAccessToken().getBytes()));// setting up accessToken as HTTP-only cookieCookie cookie = new Cookie("accessToken", encryptedAccessToken);cookie.setMaxAge((authResult.getExpiresIn() - 60) * 1000);cookie.setHttpOnly(true);cookie.setSecure(true);cookie.setPath("/");response.addCookie(cookie);// Store the refreshToken in a secure placeThis sets browser cookies with the session tokens. Every request to your backend needs to verify the
accessToken
to ensure the user is authenticated. If expired, use therefreshToken
to get a new access token.// Middleware to verify and refresh tokens if neededconst verifyToken = async (req, res, next) => {try {// Get access token from cookieconst accessToken = req.cookies.accessToken;// Decrypt the accessToken using the same encryption algorithmconst decryptedAccessToken = decrypt(accessToken);if (!accessToken) {return res.status(401).json({ message: 'No access token provided' });}// Use Scalekit SDK to validate the tokenconst isValid = await scalekit.validateAccessToken(decryptedAccessToken);if (!isValid) {// Use stored refreshToken to get a new access tokenconst {user,idToken,accessToken,refreshToken: newRefreshToken,} = await scalekit.refreshToken(refreshToken);// Store the new refresh token// Update the cookie with the new access token}next();};// Example of using the middleware to protect routesapp.get('/dashboard', verifyToken, (req, res) => {// The user object is now available in req.userres.json({message: 'This is a protected route',user: req.user});});from functools import wrapsfrom flask import request, jsonify, make_responsedef verify_token(f):"""Decorator to verify and refresh tokens if needed"""@wraps(f)def decorated_function(*args, **kwargs):try:# Get access token from cookieaccess_token = request.cookies.get('accessToken')if not access_token:return jsonify({'message': 'No access token provided'}), 401# Decrypt the accessToken using the same encryption algorithmdecrypted_access_token = decrypt(access_token)# Use Scalekit SDK to validate the tokenis_valid = scalekit.validate_access_token(decrypted_access_token)if not is_valid:# Get stored refresh tokenrefresh_token = get_stored_refresh_token()if not refresh_token:return jsonify({'message': 'No refresh token available'}), 401# Use stored refreshToken to get a new access tokentoken_response = scalekit.refresh_token(refresh_token)# Python SDK returns dict with access_token and refresh_tokennew_access_token = token_response.get('access_token')new_refresh_token = token_response.get('refresh_token')# Store the new refresh tokenstore_refresh_token(new_refresh_token)# Update the cookie with the new access tokenencrypted_new_access_token = encrypt(new_access_token)response = make_response(f(*args, **kwargs))response.set_cookie('accessToken',encrypted_new_access_token,httponly=True,secure=True,path='/',samesite='Strict')return response# If the token was valid we just invoke the view as-isreturn f(*args, **kwargs)except Exception as e:return jsonify({'message': f'Token verification failed: {str(e)}'}), 401return decorated_function# Example of using the decorator to protect routes@app.route('/dashboard')@verify_tokendef dashboard():return jsonify({'message': 'This is a protected route','user': getattr(request, 'user', None)})import ("context""net/http")func verifyToken(next http.HandlerFunc) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {// Get access token from cookiecookie, err := r.Cookie("accessToken")if err != nil {http.Error(w, `{"message": "No access token provided"}`, http.StatusUnauthorized)return}accessToken := cookie.Value// Decrypt the accessTokendecryptedAccessToken, err := decrypt(accessToken)if err != nil {http.Error(w, `{"message": "Token decryption failed"}`, http.StatusUnauthorized)return}// Use Scalekit SDK to validate the tokenisValid, err := scalekit.ValidateAccessToken(decryptedAccessToken)if err != nil || !isValid {// Get stored refresh tokenrefreshToken, err := getStoredRefreshToken(r)if err != nil {http.Error(w, `{"message": "No refresh token available"}`, http.StatusUnauthorized)return}// Use stored refreshToken to get a new access tokentokenResponse, err := scalekit.RefreshToken(refreshToken)if err != nil {http.Error(w, `{"message": "Token refresh failed"}`, http.StatusUnauthorized)return}// Go SDK returns TokenResponse with AccessToken, RefreshToken, ExpiresIn// Store the new refresh tokenerr = storeRefreshToken(tokenResponse.RefreshToken)if err != nil {http.Error(w, `{"message": "Failed to store refresh token"}`, http.StatusInternalServerError)return}// Update the cookie with the new access tokenencryptedNewAccessToken, err := encrypt(tokenResponse.AccessToken)if err != nil {http.Error(w, `{"message": "Token encryption failed"}`, http.StatusInternalServerError)return}newCookie := &http.Cookie{Name: "accessToken",Value: encryptedNewAccessToken,HttpOnly: true,Secure: true,Path: "/",SameSite: http.SameSiteStrictMode,}http.SetCookie(w, newCookie)r = r.WithContext(context.WithValue(r.Context(), "tokenValid", true))} else {r = r.WithContext(context.WithValue(r.Context(), "tokenValid", true))}next(w, r)}}// Example of using the middleware to protect routesfunc dashboardHandler(w http.ResponseWriter, r *http.Request) {w.Header().Set("Content-Type", "application/json")w.Write([]byte(`{"message": "This is a protected route","tokenValid": true}`))}// Usage: http.HandleFunc("/dashboard", verifyToken(dashboardHandler))import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.Cookie;import org.springframework.web.servlet.HandlerInterceptor;@Componentpublic class TokenVerificationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {try {// Get access token from cookieString accessToken = getCookieValue(request, "accessToken");if (accessToken == null) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"message\": \"No access token provided\"}");return false;}// Decrypt the accessTokenString decryptedAccessToken = decrypt(accessToken);// Use Scalekit SDK to validate the tokenboolean isValid = scalekit.validateAccessToken(decryptedAccessToken);if (!isValid) {// Get stored refresh tokenString refreshToken = getStoredRefreshToken(request);if (refreshToken == null) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"message\": \"No refresh token available\"}");return false;}// Use stored refreshToken to get a new access tokenAuthenticationResponse tokenResponse = scalekit.refreshToken(refreshToken);// Store the new refresh tokenstoreRefreshToken(tokenResponse.getRefreshToken());// Update the cookie with the new access tokenString encryptedNewAccessToken = encrypt(tokenResponse.getAccessToken());Cookie newCookie = new Cookie("accessToken", encryptedNewAccessToken);newCookie.setHttpOnly(true);newCookie.setSecure(true);newCookie.setPath("/");response.addCookie(newCookie);request.setAttribute("tokenRefreshed", true);} else {request.setAttribute("tokenValid", true);}return true;} catch (Exception e) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"message\": \"Token verification failed: " + e.getMessage() + "\"}");return false;}}private String getCookieValue(HttpServletRequest request, String cookieName) {Cookie[] cookies = request.getCookies();if (cookies != null) {for (Cookie cookie : cookies) {if (cookieName.equals(cookie.getName())) {return cookie.getValue();}}}return null;}}// Example of using the interceptor to protect routes@RestControllerpublic class DashboardController {@GetMapping("/dashboard")public ResponseEntity<Map<String, Object>> dashboard(HttpServletRequest request) {Map<String, Object> response = new HashMap<>();response.put("message", "This is a protected route");response.put("tokenValid", request.getAttribute("tokenValid"));return ResponseEntity.ok(response);}}Successfully authenticated users can now access your dashboard.
-
Log out the user
Section titled “Log out the user”To properly log out users, clear local session data and invalidate their session on Scalekit’s servers:
/*** Handles user logout by:* 1. Clearing local session data* 2. Invalidating the Scalekit session* 3. Redirecting to post-logout URL*/app.get('/logout', (req, res) => {// Clear all session data including cookies and local storageclearSessionData();/*** Generates a Scalekit logout URL that will:* - Invalidate the user's session on Scalekit's servers* - Redirect the user to the specified post-logout URL* @param {string} idToken - The user's ID token to invalidate* @param {string} postLogoutRedirectUri - URL to redirect after logout* @returns {string} The complete logout URL*/const logoutUrl = scalekit.getLogoutUrl(idToken,postLogoutRedirectUri);// Redirect to Scalekit's logout endpoint// Note: This is a one-time use URL that becomes invalid after useres.redirect(logoutUrl);});from flask import Flask, redirectfrom scalekit import LogoutUrlOptionsapp = Flask(__name__)@app.route('/logout')def logout():# Clear all session data including cookies and local storageclear_session_data()# Generate Scalekit logout URLoptions = LogoutUrlOptions(id_token_hint=id_token,post_logout_redirect_uri=post_logout_redirect_uri)logout_url = scalekit.get_logout_url(options)# Redirect to Scalekit's logout endpoint# Note: This is a one-time use URL that becomes invalid after usereturn redirect(logout_url)package mainimport ("net/http""github.com/gin-gonic/gin""github.com/scalekit-inc/scalekit-sdk-go")func logoutHandler(c *gin.Context) {// Clear all session data including cookies and local storageclearSessionData()// Generate Scalekit logout URLoptions := scalekit.LogoutUrlOptions{IdTokenHint: idToken,PostLogoutRedirectUri: postLogoutRedirectUri,}logoutUrl, err := scalekit.GetLogoutUrl(options)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate logout URL",})return}// Redirect to Scalekit's logout endpoint// Note: This is a one-time use URL that becomes invalid after usec.Redirect(http.StatusFound, logoutUrl.String())}import com.scalekit.internal.http.LogoutUrlOptions;import org.springframework.web.bind.annotation.*;import org.springframework.web.servlet.view.RedirectView;import java.net.URL;@RestControllerpublic class LogoutController {@GetMapping("/logout")public RedirectView logout() {// Clear all session data including cookies and local storageclearSessionData();// Generate Scalekit logout URLLogoutUrlOptions options = new LogoutUrlOptions();options.setIdTokenHint(idToken);options.setPostLogoutRedirectUri(postLogoutRedirectUri);URL logoutUrl = scalekit.authentication().getLogoutUrl(options);// Redirect to Scalekit's logout endpoint// Note: This is a one-time use URL that becomes invalid after usereturn new RedirectView(logoutUrl.toString());}}The logout process completes when Scalekit invalidates the user’s session and redirects them to your specified post-logout URL.
What you’ve accomplished
Section titled “What you’ve accomplished”🎉 Congratulations! You’ve successfully integrated Scalekit’s Full Stack Authentication. Your application now has:
- Complete authentication flow - Sign-in, sign-up, and logout
- Secure session management - Encrypted tokens with automatic refresh
- Multi-tenant architecture - Ready for B2B SaaS scaling
- Enterprise-ready foundation - Built to handle SSO and advanced auth methods
What’s next?
Section titled “What’s next?”You’ve completed the Scalekit quickstart and enabled secure authentication for your users and sign-in flow.
- Design your data model to learn how to model your data to best work with Scalekit.
- Manage users to create, update, and delete user accounts.
- Customize the login page to match your brand’s design and style.