Login Implementation
This document explains the implementation of login functionality using the Auth API sample application.
For an overview of the Auth API and its flow, see Auth API Implementation Guide Overview.
Frontend Implementation
Login Screen Component
The login screen implements the following elements:
- Email address (or ID) input field: Enter the user identifier
- Password input field: Enter the user password
- Login button: Initiate the sign-in process
- Error message area: Display authentication error messages
Login Flow
The frontend login flow is implemented as follows:
1. Initiate Sign-in
Send the user-entered email address (or ID) and password to the backend POST /sign-in endpoint.
// Login form submission handler
const handleLogin = async (identifier: string, password: string) => {
try {
const response = await fetch("/sign-in", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identifier, password }),
});
const data = await response.json();
if (data.challenge_name) {
// Challenge returned, proceed to next step
await handleChallenge(data);
}
} catch (error) {
setErrorMessage("Login failed");
}
};
2. Handle Challenge Response
Using the challenge parameters returned from the backend, send the challenge response to the backend POST /sign-in/challenge.
const handleChallenge = async (challengeData: ChallengeResponse) => {
if (challengeData.challenge_name === "NEW_PASSWORD_REQUIRED") {
// First login: navigate to new password setting page
navigateToNewPasswordPage(challengeData);
return;
}
// For PASSWORD_VERIFIER: send challenge response
const response = await fetch("/sign-in/challenge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
challenge_name: challengeData.challenge_name,
// Forward challenge parameters received from backend
...challengeData.challenge_parameters,
}),
});
if (response.ok) {
// Login successful: tokens are automatically saved in HttpOnly cookies
navigateToMainPage();
}
};
3. New Password Setting (First Login)
When a NEW_PASSWORD_REQUIRED challenge is returned, prompt the user for a new password and send it to the backend.
const handleNewPassword = async (
newPassword: string,
session: string
) => {
const response = await fetch("/sign-in/challenge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
challenge_name: "NEW_PASSWORD_REQUIRED",
new_password: newPassword,
session: session,
}),
});
if (response.ok) {
// Navigate to main page after successful password change
navigateToMainPage();
}
};
4. Post-Login Navigation
After token storage is complete, navigate to the main page (e.g., user list page). Since tokens are stored in HttpOnly Cookies, there is no need to handle tokens directly on the frontend.
Backend Implementation
The backend receives requests from the frontend, communicates with the SaaSus Platform Auth API, and processes the login flow.
POST /sign-in Endpoint
Calls the SaaSus Platform Auth API /sign-in using the email address (or ID) and password received from the frontend.
Processing Flow
- Extract email address (or ID) and password from the request body
- ID/Email conversion: If the input doesn't contain
@, append a dummy domain to convert to email format - Calculate SRP_A and call the SaaSus Platform Auth API
/sign-in - Return the challenge response (
PASSWORD_VERIFIER, etc.) to the frontend
// POST /sign-in handler
func signIn(c echo.Context) error {
var req SignInRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
// ID/email conversion
email := req.Identifier
if !strings.Contains(req.Identifier, "@") {
// For ID format, append dummy domain
email = req.Identifier + "@example.auth"
}
// Call SaaSus Platform Auth API /sign-in
// Calculates and sends SRP_A
resp, err := authClient.SignIn(email, req.Password)
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "authentication failed"})
}
// Return challenge parameters to frontend
return c.JSON(http.StatusOK, resp)
}
When using ID-based login, ensure the dummy domain matches the one used when registering users in SaaSus Platform. It is recommended to manage this via environment variables.
POST /sign-in/challenge Endpoint
Calls the SaaSus Platform Auth API /sign-in/challenge using the challenge response received from the frontend. Processes are branched based on the challenge type.
PASSWORD_VERIFIER Case
The standard password verification flow. Send the challenge response to obtain tokens.
// POST /sign-in/challenge handler
func signInChallenge(c echo.Context) error {
var req ChallengeRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
// Call SaaSus Platform Auth API /sign-in/challenge
resp, err := authClient.RespondToChallenge(req)
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "challenge failed"})
}
switch resp.ChallengeName {
case "":
// Authentication complete: set tokens in HttpOnly cookies
setTokenCookies(c, resp.Tokens)
return c.JSON(http.StatusOK, map[string]string{"status": "authenticated"})
case "NEW_PASSWORD_REQUIRED":
// First login: new password setting required
return c.JSON(http.StatusOK, map[string]interface{}{
"challenge_name": "NEW_PASSWORD_REQUIRED",
"session": resp.Session,
"challenge_parameters": resp.ChallengeParameters,
})
default:
return c.JSON(http.StatusBadRequest, map[string]string{"error": "unsupported challenge"})
}
}
NEW_PASSWORD_REQUIRED Case
Handles the case where a password change is required on first login. Receives the new password and sends it to the SaaSus Platform Auth API.
// New password setting handler
// Called when challenge_name is NEW_PASSWORD_REQUIRED
func handleNewPasswordRequired(c echo.Context, req ChallengeRequest) error {
// Send new password to SaaSus Platform Auth API
resp, err := authClient.RespondToNewPasswordChallenge(
req.Session,
req.NewPassword,
)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "password change failed"})
}
// Set tokens in HttpOnly cookies after successful password change
setTokenCookies(c, resp.Tokens)
return c.JSON(http.StatusOK, map[string]string{"status": "authenticated"})
}
Even in the NEW_PASSWORD_REQUIRED flow, if the user logged in with an ID format, you need to correctly handle the email address with the appended dummy domain. It is recommended to persist the converted value in the session from the initial sign-in step.
Security Implementation
Token Management with HttpOnly Cookies
Store obtained tokens in HttpOnly Cookies. Compared to storing in localStorage, this reduces the risk of token theft through XSS (Cross-Site Scripting) attacks.
// Function to set tokens in HttpOnly cookies
func setTokenCookies(c echo.Context, tokens Tokens) {
// Access token
c.SetCookie(&http.Cookie{
Name: "access_token",
Value: tokens.AccessToken,
Path: "/",
HttpOnly: true,
Secure: true, // Always true in HTTPS environments
SameSite: http.SameSiteStrictMode,
MaxAge: 3600, // 1 hour
})
// Refresh token
c.SetCookie(&http.Cookie{
Name: "refresh_token",
Value: tokens.RefreshToken,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
MaxAge: 86400 * 30, // 30 days
})
// ID token
c.SetCookie(&http.Cookie{
Name: "id_token",
Value: tokens.IDToken,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
MaxAge: 3600, // 1 hour
})
}
Cookie Attribute Configuration
| Attribute | Value | Description |
|---|---|---|
HttpOnly | true | Prevents JavaScript access, protecting tokens from XSS attacks |
Secure | true | Only sends cookies over HTTPS connections (required in production) |
SameSite | Strict | Prevents cookie transmission in cross-site requests |
Path | / | Makes cookies available across the entire application |
In local development environments (http://localhost), you may need to set the Secure attribute to false. It is recommended to make this configurable via environment variables.
CSRF Protection
When using HttpOnly Cookies, CSRF (Cross-Site Request Forgery) protection is required. This sample combines the following measures:
- SameSite Cookie Attribute: Set
SameSite=Strictto prevent cookie transmission from cross-site requests - CSRF Token: Generate a CSRF token on the server side, pass it to the frontend, and verify it in the request header
// CSRF middleware configuration example
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "header:X-CSRF-Token",
CookiePath: "/",
}))
Logout
On logout, clear all tokens stored in HttpOnly Cookies.
// POST /sign-out handler
func signOut(c echo.Context) error {
// Delete each token cookie (set MaxAge to -1)
for _, name := range []string{"access_token", "refresh_token", "id_token"} {
c.SetCookie(&http.Cookie{
Name: name,
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
MaxAge: -1, // Delete cookie immediately
})
}
return c.JSON(http.StatusOK, map[string]string{"status": "signed_out"})
}
Frontend logout call:
const handleLogout = async () => {
await fetch("/sign-out", { method: "POST" });
// Navigate to login page
navigateToLoginPage();
};
ID Login Extension
You can implement an ID authentication method that uses username + tenant ID instead of an email address.
ID login can be implemented on its own or alongside email login. The SRP authentication calculation and token verification (POST /verify) mechanism is the same as email login; only the challenge request endpoint and parameters differ.
Overview and Prerequisites
ID login leverages the login_domain tenant attribute configured in SaaSus Platform. The backend retrieves tenant information and concatenates username + login_domain (e.g., taro + @example.com = taro@example.com) before sending it to the SaaSus Platform Auth API.
Prerequisites
- The tenant attribute
login_domainmust be defined in the SaaS Operation Console (e.g.,@example.com) - The target tenant must have a
login_domainvalue configured - Users must be registered in SaaSus Platform in the format
username + login_domain
Frontend Input Fields
For ID login, the frontend accepts the following inputs:
- Username: The part before
@in the email address (e.g.,taro) - Tenant ID: The tenant ID to log in to
- Password: The user's password (used only for SRP calculation, never sent to the server)
The tenant ID can also be pre-specified via the URL query parameter ?tenant_id=xxx. This enables use cases such as distributing tenant-specific login URLs.
Challenge Request
For ID login, call the /challenge-id endpoint instead of the /challenge endpoint used for email login.
// Challenge request for ID login
const challengeResponse = await apiClient.post('/challenge-id', {
user_id: userId,
tenant_id: tenantId,
srp_a: srpA,
});
The processing after obtaining the challenge (SRP signature calculation, token retrieval via POST /verify) is the same as email login.
POST /challenge-id Endpoint
The challenge endpoint for ID authentication. It accepts user_id and tenant_id instead of an email address.
Request Structure
// ChallengeIdRequest is the request structure for ID authentication challenge
type ChallengeIdRequest struct {
UserID string `json:"user_id" binding:"required"`
TenantID string `json:"tenant_id" binding:"required"`
SrpA string `json:"srp_a" binding:"required"`
}
Processing Flow
- Extract
user_id,tenant_id, andsrp_afrom the request body - Retrieve tenant information: Get tenant info via the SaaSus Platform Auth API and read the
login_domainfrom tenant attributes - Construct USERNAME: Concatenate
user_id + login_domain(e.g.,taro+@example.com=taro@example.com) - Call the SaaSus Platform Auth API
/sign-into initiate SRP authentication - Return challenge parameters to the frontend
func challengeId(c echo.Context) error {
var req ChallengeIdRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, ChallengeResponse{
Success: false,
Message: "Invalid request format",
})
}
ctx := context.Background()
// Retrieve tenant information and read login_domain attribute
tenantResponse, err := authClient.GetTenantWithResponse(ctx, req.TenantID)
if err != nil || tenantResponse.JSON200 == nil {
return c.JSON(http.StatusBadRequest, ChallengeResponse{
Success: false,
Message: "Tenant not found",
})
}
// Get login_domain from tenant attributes
tenant := tenantResponse.JSON200
loginDomain := ""
if tenant.Attributes != nil {
if domain, ok := tenant.Attributes["login_domain"]; ok {
if domainStr, ok := domain.(string); ok {
loginDomain = domainStr
}
}
}
if loginDomain == "" {
return c.JSON(http.StatusBadRequest, ChallengeResponse{
Success: false,
Message: "Tenant does not have login_domain attribute configured",
})
}
// Construct USERNAME from user_id + login_domain
// e.g., "taro" + "@example.com" = "taro@example.com"
username := req.UserID + loginDomain
// Send SignIn request to SaaSus Platform Auth API
signInParam := authapi.SignInParam{
SignInFlow: authapi.USERSRPAUTH,
SignInParameters: &map[string]string{
"USERNAME": username,
"SRP_A": req.SrpA,
},
}
signInResponse, err := authClient.SignInWithResponse(ctx, signInParam)
if err != nil || signInResponse.JSON200 == nil {
return c.JSON(http.StatusInternalServerError, ChallengeResponse{
Success: false,
Message: "SignIn challenge failed",
})
}
// Return challenge parameters (subsequent verify flow is shared with email login)
signInResult := signInResponse.JSON200
challengeParameters := *signInResult.ChallengeParameters
session := ""
if signInResult.Session != nil {
session = *signInResult.Session
}
return c.JSON(http.StatusOK, ChallengeResponse{
Success: true,
Message: "Challenge parameters retrieved",
SrpB: challengeParameters["SRP_B"],
Salt: challengeParameters["SALT"],
SecretBlock: challengeParameters["SECRET_BLOCK"],
PoolName: challengeParameters["POOL_NAME"],
Username: challengeParameters["USER_ID_FOR_SRP"],
Session: session,
})
}
Set login_domain in a format that includes @ and the domain (e.g., @example.com). This ensures that when concatenated with user_id, it forms a valid email address format.
ID Login Processing Flow
The main difference from email login is in steps 2–4. The process of retrieving login_domain from tenant information and concatenating it with user_id to construct the USERNAME is added. Steps 7 onward (SRP signature calculation and token retrieval via POST /verify) are shared.
Summary
This document explained the implementation of login functionality using the SaaSus Platform Auth API. Key points:
- SRP protocol-based two-step authentication flow: Obtain a challenge via
/sign-inand respond via/sign-in/challenge - ID login extension: Leverage the
login_domaintenant attribute to enable login with username + tenant ID NEW_PASSWORD_REQUIREDhandling: Implement the password change flow for first-time login- Secure token management with HttpOnly Cookies: Store tokens in cookies as XSS protection
- CSRF protection and logout: Protection via SameSite attribute and CSRF tokens, session termination via cookie clearing