Skip to content

Login Quickstart

Add "Sign in with RemiliaNET" to your app. By the end of this guide, a user can sign in to your application, and your frontend can call the API on their behalf.

Login clients are public, and intended to be embedded on your application's frontend. They allow you to make requests to the API on behalf of a user, after that user has signed in to your application. To act only as your application, with no user involved, use an API client (Client Credentials) instead. See the API Quickstart.

Register your login client

  1. Sign in at https://www.remilia.net and open the developer portal from your profile menu.
  2. Create an application and pick the Login (public) client type.
  3. Register every redirect URI your app will use. The redirect_uri you send in a login flow must match a registered value verbatim, or the authorization endpoint rejects it. (For an SPA that is the app's base URL — see step 1 below.)
  4. Select the scopes your integration needs from the scope reference.
  5. Submit. Your application page shows the assigned client ID — the only credential a login client has. Never embed a client secret in one.

React quickstart with oidc-spa

1. Register your app URLs

Register the app's base URL, including the trailing slash, as a redirect URI:

text
https://yourapp.example/
http://localhost:5173/

2. Install oidc-spa

sh
npm install oidc-spa@^10

For Vite, add the oidc-spa plugin:

ts
// vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { oidcSpa } from "oidc-spa/vite-plugin";

export default defineConfig({
  plugins: [react(), oidcSpa()],
});

3. Configure the login client

dotenv
# .env.local
VITE_REMILIA_CLIENT_ID=YOUR_LOGIN_CLIENT_ID
ts
// src/oidc.ts
import { oidcSpa } from "oidc-spa/react-spa";

export const {
  bootstrapOidc,
  useOidc,
  getOidc,
  OidcInitializationGate,
} = oidcSpa.createUtils();

void bootstrapOidc({
  implementation: "real",
  issuerUri: "https://www.remilia.net/oidc/realms/remilia",
  clientId: import.meta.env.VITE_REMILIA_CLIENT_ID,
});

4. Gate the app and add login controls

tsx
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import { OidcInitializationGate } from "./oidc";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <OidcInitializationGate fallback={<p>Loading session…</p>}>
      <App />
    </OidcInitializationGate>
  </StrictMode>,
);
tsx
// src/App.tsx
import { useOidc } from "./oidc";

export function App() {
  const oidc = useOidc();

  if (!oidc.isUserLoggedIn) {
    return (
      <button type="button" onClick={() => void oidc.login()}>
        Sign in with RemiliaNET
      </button>
    );
  }

  return (
    <button
      type="button"
      onClick={() => void oidc.logout({ redirectTo: "home" })}
    >
      Sign out
    </button>
  );
}

5. Call the API

ts
import { getOidc } from "./oidc";

const oidc = await getOidc({ assert: "user logged in" });
const accessToken = await oidc.getAccessToken();

const response = await fetch("https://www.remilia.net/api/v1/me", {
  headers: { Authorization: `Bearer ${accessToken}` },
});

getAccessToken() renews the token when needed. See Endpoints for requests and Scopes for permissions.

Manual Authorization Code + PKCE

Use the manual flow when oidc-spa does not fit your application.

OIDC discovery

Issuer:

https://www.remilia.net/oidc/realms/remilia

Read endpoints from the discovery document at runtime rather than hardcoding them:

https://www.remilia.net/oidc/realms/remilia/.well-known/openid-configuration
Key in discovery documentValue
authorization_endpointhttps://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/auth
token_endpointhttps://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/token
end_session_endpointhttps://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/logout

Step 1 — Generate state, PKCE verifier, and S256 challenge

Persist verifier and state before redirecting; you need them on the callback.

js
// base64url-encode raw bytes (no padding), per RFC 7636.
function base64url(bytes) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));

const digest = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode(verifier),
);
const challenge = base64url(new Uint8Array(digest));

sessionStorage.setItem("oauth_state", state);
sessionStorage.setItem("pkce_verifier", verifier);

Step 2 — Redirect to the authorization endpoint

Send the user to authorization_endpoint with these parameters:

ParameterValue
client_idYour registered login client id
response_typecode
redirect_uriA URI registered for your client, sent verbatim
scopeopenid — your client's remilia: scopes are attached automatically
stateThe random value from Step 1
code_challengeThe S256 challenge from Step 1
code_challenge_methodS256
js
const params = new URLSearchParams({
  client_id: "YOUR_CLIENT_ID",
  response_type: "code",
  redirect_uri: "https://yourapp.example/callback",
  scope: "openid",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
});

window.location.assign(
  "https://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/auth" +
    `?${params}`,
);

The /callback path above is only an example — send any URI registered for your client.

Step 3 — Validate the callback

On success RemiliaNET returns ?code=…&state=…; on failure, ?error=…&error_description=…. Confirm the returned state equals the stored value before proceeding, and reject mismatches.

js
const url = new URL(window.location.href);

const error = url.searchParams.get("error");
if (error) {
  throw new Error(`${error}: ${url.searchParams.get("error_description")}`);
}

const returnedState = url.searchParams.get("state");
if (!returnedState || returnedState !== sessionStorage.getItem("oauth_state")) {
  throw new Error("state mismatch — possible CSRF, aborting");
}

const code = url.searchParams.get("code");

Step 4 — Exchange the code for tokens

POST to token_endpoint as application/x-www-form-urlencoded.

js
const res = await fetch(
  "https://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/token",
  {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      client_id: "YOUR_CLIENT_ID",
      code,
      redirect_uri: "https://yourapp.example/callback", // must match Step 2
      code_verifier: sessionStorage.getItem("pkce_verifier"),
    }),
  },
);

if (!res.ok) {
  throw new Error(`token exchange failed: ${res.status}`);
}

const tokens = await res.json();
// tokens.access_token   — the bearer token for /api/v1
// tokens.expires_in     — access-token lifetime in seconds
// tokens.refresh_token  — renew without another redirect (Step 6)
// tokens.scope          — the scope set issued with this token
// tokens.id_token       — identity of the signed-in user (OIDC)

sessionStorage.removeItem("pkce_verifier");
sessionStorage.removeItem("oauth_state");

With curl:

sh
curl -X POST \
  https://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/token \
  -d grant_type=authorization_code \
  -d client_id=YOUR_CLIENT_ID \
  -d code=AUTHORIZATION_CODE \
  -d redirect_uri=https://yourapp.example/callback \
  -d code_verifier=YOUR_PKCE_VERIFIER

tokens.scope lists the scopes your client was granted. Calls to an endpoint whose scope is outside that grant fail with insufficient_scope (Step 5).

Step 5 — Call the API with the bearer token

Send the access token on any endpoint that requires a scope:

sh
curl https://www.remilia.net/api/v1/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Step 6 — Refresh the token

Access tokens are short-lived. Exchange the refresh_token for a new access token instead of repeating the authorization flow:

js
const res = await fetch(
  "https://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/token",
  {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      client_id: "YOUR_CLIENT_ID",
      refresh_token: storedRefreshToken,
    }),
  },
);

const tokens = await res.json(); // new access_token (+ possibly a new refresh_token)

Store the refresh_token from the latest response; when rotation is active, the old one stops working. Refresh before expires_in elapses. If the refresh token has expired or been revoked, the endpoint returns invalid_grant – fall back to the full login flow (Step 2).

Logout

Send the user to end_session_endpoint:

ParameterValue
client_idYour login client id
post_logout_redirect_uriWhere to return the user after logout — must match a registered redirect URI verbatim
id_token_hintThe id_token from Step 4 (recommended)
js
const params = new URLSearchParams({
  client_id: "YOUR_CLIENT_ID",
  post_logout_redirect_uri: "https://yourapp.example/",
  id_token_hint: storedIdToken,
});

window.location.assign(
  "https://www.remilia.net/oidc/realms/remilia/protocol/openid-connect/logout" +
    `?${params}`,
);

Clearing your local tokens signs the user out of your app; calling end_session_endpoint also ends their RemiliaNET session.

Troubleshooting

WhereSymptomCause and fix
Authorization redirecterror=invalid_redirect_uri (or an authorization error page)redirect_uri does not match one registered for your client. Send a registered value verbatim.
Authorization redirectThe sign-in page reports an unknown or invalid clientThe application is not approved yet, or client_id is mistyped. Check the application's status in the developer portal.
Callbackstate mismatch in your own checkThe response is not tied to your request, or the browser lost the stored state. Restart the flow.
Token exchangeHTTP 400 invalid_grantThe code was already used or expired, the redirect_uri differs from Step 2, or the code_verifier is missing/wrong. Start a fresh authorization request.
Token refreshHTTP 400 invalid_grantThe refresh token expired or was revoked. Fall back to the full login flow (Step 2).
API call401 invalid_tokenThe bearer token is malformed or expired. Refresh it (Step 6) and retry.
API call401 unauthorizedNo valid token on a route that requires one. Send Authorization: Bearer <access_token>.
API call403 insufficient_scopeThe endpoint requires a scope outside your client's grant. Inspect tokens.scope, then apply for the scope in the developer portal and have the user sign in again. See Scopes.
API call403 requires_userYou used a token with the required scope but no user attached — an app-only API client token. Endpoints acting for a specific user require a user-delegated token from this login flow.

Built by Remilia Corporation