SeleID

Quickstart

React Native and Expo

Overview · React Native and Expo · Android · iOS · Register an app · API reference · Errors and security · Changelog

Coming soon @seletech/seleid-client is not on npm yet. These pages describe version 0.1.0 so you can plan an integration. See the roadmap.

Runs on React Native with a global fetch. Local token verification also needs crypto.subtle or a CryptoPort you pass in (Planned on the server side, see the overview). The SDK imports no Expo module; you pass storage in.

1. Install

npx expo install expo-secure-store expo-crypto expo-web-browser
npm install @seletech/seleid-client   # planned, not published yet

2. Create the client

seleid.ts

import {
  SeleIdClient,
  TokenManager,
  SecureStoreTokenStore,
  SessionGuard,
} from '@seletech/seleid-client';
import * as SecureStore from 'expo-secure-store';

const CLIENT_ID = 'your-client-id'; // public; mobile apps are public clients and have no secret

export const seleid = new SeleIdClient({
  // baseUrl defaults to https://auth.seleid.dev
  // timeoutMs: 4000, retries: 2, budgetMs: 10000 (all optional)
  onEvent: (e) => console.debug('seleid', e.type, e.op, e.durationMs, e.errorKind), // no tokens or personal data in events
});

export const tokens = new TokenManager({
  client: seleid,
  clientId: CLIENT_ID,
  store: new SecureStoreTokenStore(SecureStore), // key seleid.tokens.v1
});

export const verifier = seleid.createVerifier({ audience: CLIENT_ID });
export const guard = new SessionGuard({ client: seleid, tokens, verifier });

3. Sign in (authorization code with PKCE)

SDK 0.1.0 has no authorize-URL or PKCE helper (Planned). The helper below is app code built on expo-crypto and expo-web-browser. SeleID requires client_id, an exact registered redirect_uri, response_type=code, a scope that includes openid (allowed: openid profile email), state, a nonce of 16 to 256 characters, and an S256 code_challenge of 43 to 128 characters.

signin.ts

import * as Crypto from 'expo-crypto';
import * as WebBrowser from 'expo-web-browser';
import { seleid, tokens, CLIENT_ID } from './seleid';

const REDIRECT_URI = 'com.example.app:/seleid'; // must match a registered redirect URI exactly

function b64url(base64: string): string {
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function randomString(bytes = 32): Promise<string> {
  const raw = Crypto.getRandomBytes(bytes);
  let bin = '';
  raw.forEach((b) => (bin += String.fromCharCode(b)));
  return b64url(btoa(bin)); // 43 characters for 32 bytes
}

export async function signIn() {
  const { authorization_endpoint } = await seleid.discovery();
  const verifier = await randomString(32);
  const state = await randomString(16);
  const nonce = await randomString(16);
  const challenge = b64url(
    await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, verifier, {
      encoding: Crypto.CryptoEncoding.BASE64,
    })
  );
  const url =
    authorization_endpoint +
    '?' +
    new URLSearchParams({
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      response_type: 'code',
      scope: 'openid profile email',
      state,
      nonce,
      code_challenge: challenge,
      code_challenge_method: 'S256',
    }).toString();

  const result = await WebBrowser.openAuthSessionAsync(url, REDIRECT_URI);
  if (result.type !== 'success') return null; // cancelled
  const params = new URL(result.url).searchParams;
  if (params.get('state') !== state) throw new Error('state mismatch');
  const code = params.get('code');
  if (!code) throw new Error(params.get('error') ?? 'no_code');

  const tokenSet = await seleid.exchangeCode({
    clientId: CLIENT_ID,
    code,
    redirectUri: REDIRECT_URI,
    codeVerifier: verifier,
  });
  await tokens.setTokens(tokenSet); // { accessToken, refreshToken, idToken, scope, expiresAt }
  return tokenSet;
}

4. Check the session

import { isSeleIdError } from '@seletech/seleid-client';
import { guard } from './seleid';

// Browsing, feeds, playback: local verification only, no SeleID call.
await guard.check();

// Sensitive actions (payments, password change): online check, fails closed.
try {
  const { context, source } = await guard.check({ sensitive: true });
  // context.status === 'active'; source is 'online' or 'cached' (cache lives 30 s)
} catch (error) {
  if (!isSeleIdError(error)) throw error;
  if (error.isUnknown) {
    showRetry('We could not confirm your account. Try again.'); // timeout, network, unavailable
  } else if (error.endsSession) {
    goToSignIn(); // revoked, disabled, deleted: tokens are already cleared
  }
}

Reminder: until SeleID deploys /v1/session/context and JWT access tokens, these checks fail with a typed error in production. Build your UI for that case now.

5. Get an access token for your own API

const accessToken = await tokens.getAccessToken(); // refreshes first if less than 30 s are left
// A timeout or outage keeps the stored tokens; only a definitive refusal clears them.

6. Sign out

await tokens.clear(); // local sign-out. Server-side token revocation and end-session endpoints are Planned.

7. Test without a network

import { FakeSeleId } from '@seletech/seleid-client/testing';
import { SeleIdClient } from '@seletech/seleid-client';

const fake = await FakeSeleId.create();           // real RS256 keys
const client = new SeleIdClient({ fetch: fake.fetch });
const token = await fake.issueAccessToken();      // valid access token for the fake subject
fake.bumpGeneration();                            // simulate "sign out everywhere"
fake.setStatus('disabled');                       // simulate a disabled account
fake.failNext('/v1/session/context' /* , ...failures */);

A hosted sandbox with test accounts is Planned; FakeSeleId is the supported way to test today. A sample project repository is Planned.

Last updated 2026-11-12Suggest an editReport a problem with this page