Taler ID — OAuth 2.0 / OIDC

Integration Guide for External Applications

Overview

Taler ID is an OpenID Connect (OIDC) provider that allows external applications to authenticate users and access their identity data (profile, email, KYC status, wallet address) using the standard Authorization Code + PKCE flow.

ParameterValue
Issuerhttps://id.taler.tirol/oauth
ProtocolOpenID Connect 1.0 over OAuth 2.0
Grant typeAuthorization Code + PKCE (S256)
Token formatJWT (RS256, kid: taler-id-rsa)
Client authclient_secret_basic (HTTP Basic)

Quick Start (5 steps)

1

Register your application

Contact Taler team to get client_id, client_secret, and register your redirect_uri.

2

Redirect user to authorize

Generate PKCE code_verifier + code_challenge, then redirect to /oauth/auth.

3

User logs in and approves

Taler ID shows login form, then consent screen. User approves the requested scopes.

4

Exchange code for tokens

Your server receives code on the callback URL. Exchange it at POST /oauth/token.

5

Use access token

Call GET /oauth/me with Bearer token to get user claims, or decode the id_token JWT.

Client Registration

Any email-verified Taler ID user can register an OAuth client without contacting the Taler team. Self-registered clients are auto-approved with the default scope set (openid profile email offline_access). Sensitive scopes (kyc, wallet, phone) require a manual upgrade — email support@taler.tirol.

POST /oauth/register

curl -X POST https://id.taler.tirol/oauth/register \
  -H "Authorization: Bearer YOUR_TALER_ID_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My App",
    "redirect_uris": ["https://myapp.example/callback"],
    "logo_uri": "https://myapp.example/logo.png"
  }'

Successful response (RFC 7591):

{
  "client_id": "9d2a8e44-...-uuid",
  "client_secret": "base64url-secret",
  "client_id_issued_at": 1714293000,
  "client_secret_expires_at": 0,
  "client_name": "My App",
  "redirect_uris": ["https://myapp.example/callback"],
  "scope": "openid profile email offline_access",
  "token_endpoint_auth_method": "client_secret_basic",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}

Manage your clients

Limits: 10 clients per user, 3 registrations per minute per IP.

Lost your client_secret? The secret is shown only at registration. To rotate, delete the client and register a new one (a dedicated rotation endpoint is planned).

Working Example: Node.js + Express

🎮 Try it live — no installation needed

We host a live instance of this exact demo on the same server. Click below to log in with your Taler ID account and see the full Authorization Code + PKCE flow end-to-end — including a userinfo fetch from /oauth/me.

→ Open Live Demo

Below is the same demo as a full source listing — copy, run locally, and adapt to your own app. Uses the standard openid-client library which auto-discovers all endpoints from /oauth/.well-known/openid-configuration.

Step 1 — Register the client

Use any logged-in Taler ID user (with email_verified=true) to register your demo client. redirect_uris must include the local callback URL:

curl -X POST https://id.taler.tirol/oauth/register \
  -H "Authorization: Bearer YOUR_TALER_ID_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Demo App",
    "redirect_uris": ["http://localhost:3000/callback"]
  }'

Copy client_id and client_secret from the response — you'll need them in step 3.

Step 2 — Project files

Create a fresh directory taler-id-demo/ with three files:

package.json

{
  "name": "taler-id-demo",
  "version": "1.0.0",
  "type": "module",
  "scripts": { "start": "node server.js" },
  "dependencies": {
    "express": "^4.18.0",
    "openid-client": "^5.6.0",
    "dotenv": "^16.0.0"
  }
}

.env

TALER_ISSUER=https://id.taler.tirol/oauth
TALER_CLIENT_ID=<paste from step 1>
TALER_CLIENT_SECRET=<paste from step 1>
APP_BASE_URL=http://localhost:3000

server.js

import express from 'express';
import { Issuer, generators } from 'openid-client';
import 'dotenv/config';

const app = express();
const PORT = 3000;

// 1. Discover Taler ID OIDC endpoints automatically.
const issuer = await Issuer.discover(process.env.TALER_ISSUER);

// 2. Build a client with your registered credentials.
const client = new issuer.Client({
  client_id: process.env.TALER_CLIENT_ID,
  client_secret: process.env.TALER_CLIENT_SECRET,
  redirect_uris: [`${process.env.APP_BASE_URL}/callback`],
  response_types: ['code'],
});

// 3. In-memory PKCE store (use Redis or signed cookies in production).
const pendingLogins = new Map();

// 4. Landing page with the "Login with Taler ID" button.
app.get('/', (_req, res) => {
  res.send(`
    <h1>Demo App</h1>
    <a href="/login" style="
      display: inline-block; padding: 12px 24px;
      background: #167EF2; color: white;
      text-decoration: none; border-radius: 8px;
      font-family: system-ui;
    ">Login with Taler ID</a>
  `);
});

// 5. Start the OAuth flow: generate PKCE + redirect to /oauth/auth.
app.get('/login', (req, res) => {
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);
  const state = generators.state();
  pendingLogins.set(state, code_verifier);

  const url = client.authorizationUrl({
    scope: 'openid profile email',
    code_challenge,
    code_challenge_method: 'S256',
    state,
  });
  res.redirect(url);
});

// 6. Receive the authorization code, exchange for tokens, fetch userinfo.
app.get('/callback', async (req, res) => {
  const params = client.callbackParams(req);
  const code_verifier = pendingLogins.get(params.state);
  pendingLogins.delete(params.state);

  if (!code_verifier) return res.status(400).send('Unknown state');

  const tokenSet = await client.callback(
    `${process.env.APP_BASE_URL}/callback`,
    params,
    { code_verifier, state: params.state },
  );

  const userinfo = await client.userinfo(tokenSet.access_token);

  res.send(`
    <h1>✅ Logged in</h1>
    <pre>${JSON.stringify(userinfo, null, 2)}</pre>
    <a href="/">← Back</a>
  `);
});

app.listen(PORT, () => {
  console.log(`Demo running on http://localhost:${PORT}`);
});

Step 3 — Run it

npm install
npm start
# Open http://localhost:3000 in a browser, click "Login with Taler ID"

You'll be redirected to Taler ID's login page, then to a consent screen showing your app's name and requested scopes, then back to /callback where the demo shows the user's profile (sub, email, name).

What this proves

Adapt for production: replace the in-memory pendingLogins Map with Redis or signed cookies; persist tokens in a session store; add offline_access to scopes if you need refresh tokens; add CSRF protection to forms; serve over HTTPS.

Authorization Flow

Your App Taler ID User ──────── ──────── ──── │ │ │ │ 1. Generate PKCE │ │ │ code_verifier (random) │ │ │ code_challenge (SHA256) │ │ │ │ │ │ 2. Redirect ──────────────────►│ │ │ GET /oauth/auth? │ │ │ client_id=... │ │ │ response_type=code │ │ │ scope=openid profile │ │ │ redirect_uri=... │ │ │ code_challenge=... │ │ │ code_challenge_method=S256│ │ │ state=... │ │ │ │ │ │ │ 3. Show login ────────────────►│ │ │ /oauth/interaction/:uid │ │ │ │ │ │ 4. User logs in ◄─────────────│ │ │ POST .../login │ │ │ │ │ │ 5. Show consent ──────────────►│ │ │ scopes: profile, email... │ │ │ │ │ │ 6. User approves ◄────────────│ │ │ POST .../consent │ │ │ │ │ 7. Callback ◄─────────────────│ │ │ GET redirect_uri? │ │ │ code=AUTH_CODE │ │ │ state=... │ │ │ iss=https://id.taler... │ │ │ │ │ │ 8. Token exchange ────────────►│ │ │ POST /oauth/token │ │ │ grant_type= │ │ │ authorization_code │ │ │ code=AUTH_CODE │ │ │ redirect_uri=... │ │ │ code_verifier=... │ │ │ Authorization: Basic ... │ │ │ │ │ │ 9. Tokens ◄───────────────────│ │ │ { access_token, │ │ │ id_token, │ │ │ refresh_token } │ │ │ │ │ │ 10. UserInfo ─────────────────►│ │ │ GET /oauth/me │ │ │ Authorization: Bearer ... │ │ │ │ │ │ 11. Claims ◄──────────────────│ │ │ { sub, email, name, │ │ │ kyc_status, wallet... } │ │ └────────────────────────────────┘ │

Endpoints

Core OAuth 2.0 / OIDC

MethodEndpointDescription
GET /oauth/auth Authorization endpoint — start the login/consent flow
POST /oauth/token Token endpoint — exchange code for tokens, or refresh
GET /oauth/me UserInfo endpoint — get user claims with access token
POST /oauth/token/revocation Revoke access or refresh token
GET /oauth/session/end End session (OIDC logout)
GET /oauth/jwks JSON Web Key Set (public keys for JWT verification)
GET /oauth/.well-known/openid-configuration OIDC Discovery document

Authorization Request Parameters

ParameterRequiredDescription
client_idYesYour registered client ID
response_typeYesMust be code
scopeYesSpace-separated list (must include openid)
redirect_uriYesMust exactly match a registered redirect URI
code_challengeYesBASE64URL(SHA256(code_verifier))
code_challenge_methodYesMust be S256
stateRecommendedRandom string for CSRF protection
nonceRecommendedRandom string included in id_token

Token Request Parameters

ParameterDescription
grant_typeauthorization_code or refresh_token
codeAuthorization code from callback (for authorization_code grant)
redirect_uriSame redirect_uri used in /oauth/auth
code_verifierOriginal PKCE code_verifier (43-128 characters)
refresh_tokenRefresh token (for refresh_token grant)
Authentication: Use HTTP Basic auth: Authorization: Basic base64(client_id:client_secret)

Token Lifetimes

TokenTTLNotes
Access Token15 minutesUse refresh token to get new one
ID Token1 hourJWT with user claims
Authorization Code1 minuteSingle-use, exchange immediately
Refresh Token30 daysRotated on each use
Session14 daysOIDC provider session

Scopes & Claims

ScopeClaimsDescription
openid sub Required. Returns unique user ID.
profile name, given_name, family_name, middle_name, locale, updated_at Basic profile information
email email, email_verified Email address and verification status
phone phone_number, phone_number_verified Phone number and verification status
kyc kyc_status, kyc_type, kyc_verified_at KYC verification status (NONE / PENDING / APPROVED / REJECTED)
wallet wallet_address Blockchain wallet address (Taler network)
offline_access (refresh token) Request a refresh token for long-lived access

Example UserInfo Response

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Ivan Petrov",
  "given_name": "Ivan",
  "family_name": "Petrov",
  "email": "ivan@example.com",
  "email_verified": true,
  "phone_number": "+79001234567",
  "phone_number_verified": true,
  "kyc_status": "APPROVED",
  "kyc_type": "BASIC",
  "kyc_verified_at": "2026-01-15T10:30:00.000Z",
  "wallet_address": "5EZS5Lp5bdPdvcNfzaiFNjsTbtK78qWjCZVwACZFCEWVwRRp"
}

Code Examples

Node.js / TypeScript (with openid-client)

import { Issuer, generators } from 'openid-client';

// 1. Discover provider configuration
const issuer = await Issuer.discover('https://id.taler.tirol/oauth');

// 2. Create client
const client = new issuer.Client({
  client_id: 'your-client-id',
  client_secret: 'your-client-secret',
  redirect_uris: ['https://yourapp.com/callback'],
  response_types: ['code'],
  token_endpoint_auth_method: 'client_secret_basic',
});

// 3. Generate PKCE
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
const state = generators.state();
const nonce = generators.nonce();

// 4. Build authorization URL
const authUrl = client.authorizationUrl({
  scope: 'openid profile email kyc wallet',
  code_challenge,
  code_challenge_method: 'S256',
  state,
  nonce,
});
// Redirect user to authUrl

// 5. Handle callback (on your /callback route)
const params = client.callbackParams(req);
const tokenSet = await client.callback(
  'https://yourapp.com/callback',
  params,
  { code_verifier, state, nonce }
);

console.log(tokenSet.access_token);
console.log(tokenSet.id_token);
console.log(tokenSet.claims());  // decoded id_token claims

// 6. Get user info
const userinfo = await client.userinfo(tokenSet.access_token);
console.log(userinfo);
// { sub, name, email, kyc_status, wallet_address, ... }

// 7. Refresh token
const refreshed = await client.refresh(tokenSet.refresh_token);
console.log(refreshed.access_token);

Python (with authlib)

from authlib.integrations.requests_client import OAuth2Session

client_id = 'your-client-id'
client_secret = 'your-client-secret'
redirect_uri = 'https://yourapp.com/callback'

# 1. Create session with PKCE
session = OAuth2Session(
    client_id, client_secret,
    redirect_uri=redirect_uri,
    code_challenge_method='S256',
)

# 2. Build authorization URL
auth_url, state = session.create_authorization_url(
    'https://id.taler.tirol/oauth/auth',
    scope='openid profile email kyc wallet',
)
# Redirect user to auth_url

# 3. Handle callback
token = session.fetch_token(
    'https://id.taler.tirol/oauth/token',
    authorization_response=callback_url,
)

# 4. Get user info
resp = session.get('https://id.taler.tirol/oauth/me')
userinfo = resp.json()
print(userinfo)
# {'sub': '...', 'email': '...', 'kyc_status': 'APPROVED', ...}

Flutter / Dart (with flutter_appauth)

import 'package:flutter_appauth/flutter_appauth.dart';

final appAuth = FlutterAppAuth();

// Authorize + get tokens in one step
final result = await appAuth.authorizeAndExchangeCode(
  AuthorizationTokenRequest(
    'your-client-id',
    'com.yourapp://callback',  // custom scheme redirect
    issuer: 'https://id.taler.tirol/oauth',
    scopes: ['openid', 'profile', 'email', 'kyc', 'wallet'],
  ),
);

print(result?.accessToken);
print(result?.idToken);
print(result?.refreshToken);

// Get user info
final response = await http.get(
  Uri.parse('https://id.taler.tirol/oauth/me'),
  headers: {'Authorization': 'Bearer ${result?.accessToken}'},
);

cURL (manual testing)

# 1. Generate PKCE code_verifier and code_challenge
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | head -c 43)
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | \
  openssl dgst -sha256 -binary | \
  openssl base64 | tr '+/' '-_' | tr -d '=')

# 2. Open this URL in browser (user logs in + consents)
echo "https://id.taler.tirol/oauth/auth?\
client_id=walletx&\
response_type=code&\
scope=openid%20profile%20email%20kyc%20wallet&\
redirect_uri=http://localhost:3001/auth/callback&\
code_challenge=$CODE_CHALLENGE&\
code_challenge_method=S256&\
state=test123"

# 3. After login, copy the "code" from redirect URL and exchange:
curl -X POST https://id.taler.tirol/oauth/token \
  -u "walletx:your_client_secret" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&\
code=PASTE_CODE_HERE&\
redirect_uri=http://localhost:3001/auth/callback&\
code_verifier=$CODE_VERIFIER"

# 4. Use access_token to get user info
curl -H "Authorization: Bearer ACCESS_TOKEN_HERE" \
  https://id.taler.tirol/oauth/me

# 5. Refresh token
curl -X POST https://id.taler.tirol/oauth/token \
  -u "walletx:your_client_secret" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN_HERE"

# 6. Revoke token
curl -X POST https://id.taler.tirol/oauth/token/revocation \
  -u "walletx:your_client_secret" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=ACCESS_OR_REFRESH_TOKEN"

Error Handling

Authorization Errors (redirect back to client)

ErrorDescription
invalid_requestMissing required parameter or malformed request
invalid_clientUnknown client_id
invalid_redirect_uriredirect_uri not registered for this client
invalid_scopeRequested scope not allowed for this client
access_deniedUser denied consent or aborted flow
server_errorInternal server error

Token Endpoint Errors (HTTP 400/401)

ErrorDescription
invalid_grantCode expired, already used, or invalid code_verifier
invalid_clientWrong client_secret or missing Basic auth
unsupported_grant_typeOnly authorization_code and refresh_token are supported

Interaction Errors (HTTP 401/403)

HTTPErrorDescription
401UnauthorizedInvalid email/password during login
403ForbiddenAccount locked after 5 failed login attempts (15 min cooldown)

Security Requirements

PKCE is mandatory. All authorization requests must include code_challenge and code_challenge_method=S256. Plain code challenges are not accepted.

Checklist

RequirementDetails
PKCE (S256)Generate cryptographically random code_verifier (43-128 chars), compute SHA256 challenge
State parameterUse state to prevent CSRF. Verify it matches on callback.
NonceInclude nonce in auth request, verify it in id_token
HTTPS onlyAll requests must use HTTPS in production
Redirect URI exact matchThe redirect_uri must exactly match a registered URI
Client secret protectionNever expose client_secret in client-side code. Use it only on the server.
Token storageStore tokens securely (httpOnly cookies or encrypted storage). Never in localStorage.
ID Token verificationVerify JWT signature against JWKS, check iss, aud, exp, nonce
Refresh token rotationRefresh tokens are rotated on each use. Always store the latest token.
Questions? Full API docs at id.taler.tirol/docs. Contact the Taler team for client registration and support.