Skip to main content

Basic Implementation Using the Auth API

This document explains how to implement sign-in and sign-out from a custom login UI, based on the Auth API sample application.

info

For an overview of the Auth API and its flow, see Auth API Implementation Guide.

Frontend

Login Screen Component

The login screen implements the following elements:

  • Email (or ID) input field: Enter the user's identifier
  • Password input field: Enter the user's password
  • Login button: Start the login process
  • Error message area: Display authentication errors

Login Processing Flow

The frontend login is implemented in the following sequence:

1. Start sign-in

Send the email (or ID) and password entered by the user to the backend's POST /sign-in endpoint.

// Login form submission
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) {
// If a challenge is returned, proceed to the next step
await handleChallenge(data);
}
} catch (error) {
setErrorMessage("Login failed");
}
};

2. Handle challenge response

Use the challenge parameters returned from the backend to send the challenge response to POST /sign-in/challenge.

const handleChallenge = async (challengeData: ChallengeResponse) => {
if (challengeData.challenge_name === "NEW_PASSWORD_REQUIRED") {
// First login: navigate to the new password screen
navigateToNewPasswordPage(challengeData);
return;
}

// PASSWORD_VERIFIER: send the 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 the challenge parameters received from the backend
...challengeData.challenge_parameters,
}),
});

if (response.ok) {
// On success: tokens are saved automatically in HttpOnly Cookies
navigateToMainPage();
}
};

3. Set new password (first login)

When the 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) {
// After password change, navigate to the main screen
navigateToMainPage();
}
};

4. Navigate after login

Once tokens are saved, navigate to the main screen (e.g., user list). Tokens are stored in HttpOnly Cookies, so the frontend does not need to handle them directly.

Backend

The backend receives requests from the frontend and communicates with the SaaSus Platform Auth API to perform login.

POST /sign-in endpoint

Use the email (or ID) and password received from the frontend to call the SaaSus Platform Auth API's /sign-in.

Processing flow

  1. Get the email (or ID) and password from the request body
  2. ID/email conversion: If the input does not contain @, append a dummy domain to convert it into email format
  3. Compute SRP_A and call the SaaSus Platform Auth API's /sign-in
  4. Return the challenge response (e.g., PASSWORD_VERIFIER) 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 a dummy domain
email = req.Identifier + "@example.auth"
}

// Call the SaaSus Platform Auth API /sign-in
// Compute and send 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 the frontend
return c.JSON(http.StatusOK, resp)
}
ID conversion tips

When using ID-style login, make sure this dummy domain matches the domain used when registering users on SaaSus Platform. Managing it via environment variables is recommended.

POST /sign-in/challenge endpoint

Use the challenge response received from the frontend to call the SaaSus Platform Auth API's /sign-in/challenge. Branch handling based on the challenge type.

PASSWORD_VERIFIER

A normal password verification flow. Send the challenge response and 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 the 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: a new password is 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

This is the case where a password change is required on first login. Receive the new password and send it to the SaaSus Platform Auth API.

// New password handling
// Called when challenge_name is NEW_PASSWORD_REQUIRED
func handleNewPasswordRequired(c echo.Context, req ChallengeRequest) error {
// Send the new password to the 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"})
}

// After successful password change, set tokens in HttpOnly Cookies
setTokenCookies(c, resp.Tokens)
return c.JSON(http.StatusOK, map[string]string{"status": "authenticated"})
}
Notes for ID login

Even in the NEW_PASSWORD_REQUIRED flow, when a user logs in with ID format, you must correctly handle the email constructed with the dummy domain. We recommend persisting the converted value (e.g., in session state) at sign-in start.

Sign-out

On sign-out, clear all tokens stored in HttpOnly Cookies.

Backend

// 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 the cookie immediately
})
}

return c.JSON(http.StatusOK, map[string]string{"status": "signed_out"})
}

Frontend

const handleLogout = async () => {
await fetch("/sign-out", { method: "POST" });
// Navigate to the login screen
navigateToLoginPage();
};