Integration Guide for External Applications
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.
Interactive API documentation
Provider configuration
Public keys for JWT verification
| Parameter | Value |
|---|---|
| Issuer | https://id.taler.tirol/oauth |
| Protocol | OpenID Connect 1.0 over OAuth 2.0 |
| Grant type | Authorization Code + PKCE (S256) |
| Token format | JWT (RS256, kid: taler-id-rsa) |
| Client auth | client_secret_basic (HTTP Basic) |
Contact Taler team to get client_id, client_secret, and register your redirect_uri.
Generate PKCE code_verifier + code_challenge, then redirect to /oauth/auth.
Taler ID shows login form, then consent screen. User approves the requested scopes.
Your server receives code on the callback URL. Exchange it at POST /oauth/token.
Call GET /oauth/me with Bearer token to get user claims, or decode the id_token JWT.
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.
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"]
}
GET /oauth/clients — list your clients (no client_secret returned)GET /oauth/clients/:clientId — view onePATCH /oauth/clients/:clientId — update client_name, redirect_uris, logo_uri, scopeDELETE /oauth/clients/:clientId — remove a clientLimits: 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).
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.
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.
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.
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"
}
}
.envTALER_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.jsimport 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}`);
});
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).
access_token + id_token (and refresh_token if you add offline_access to scopes)./oauth/me userinfo endpoint returns claims for the logged-in user.
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.
| Method | Endpoint | Description |
|---|---|---|
| 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 |
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your registered client ID |
response_type | Yes | Must be code |
scope | Yes | Space-separated list (must include openid) |
redirect_uri | Yes | Must exactly match a registered redirect URI |
code_challenge | Yes | BASE64URL(SHA256(code_verifier)) |
code_challenge_method | Yes | Must be S256 |
state | Recommended | Random string for CSRF protection |
nonce | Recommended | Random string included in id_token |
| Parameter | Description |
|---|---|
grant_type | authorization_code or refresh_token |
code | Authorization code from callback (for authorization_code grant) |
redirect_uri | Same redirect_uri used in /oauth/auth |
code_verifier | Original PKCE code_verifier (43-128 characters) |
refresh_token | Refresh token (for refresh_token grant) |
Authorization: Basic base64(client_id:client_secret)
| Token | TTL | Notes |
|---|---|---|
| Access Token | 15 minutes | Use refresh token to get new one |
| ID Token | 1 hour | JWT with user claims |
| Authorization Code | 1 minute | Single-use, exchange immediately |
| Refresh Token | 30 days | Rotated on each use |
| Session | 14 days | OIDC provider session |
| Scope | Claims | Description |
|---|---|---|
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 |
{
"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"
}
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);
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', ...}
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}'},
);
# 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 | Description |
|---|---|
invalid_request | Missing required parameter or malformed request |
invalid_client | Unknown client_id |
invalid_redirect_uri | redirect_uri not registered for this client |
invalid_scope | Requested scope not allowed for this client |
access_denied | User denied consent or aborted flow |
server_error | Internal server error |
| Error | Description |
|---|---|
invalid_grant | Code expired, already used, or invalid code_verifier |
invalid_client | Wrong client_secret or missing Basic auth |
unsupported_grant_type | Only authorization_code and refresh_token are supported |
| HTTP | Error | Description |
|---|---|---|
| 401 | Unauthorized | Invalid email/password during login |
| 403 | Forbidden | Account locked after 5 failed login attempts (15 min cooldown) |
code_challenge and code_challenge_method=S256. Plain code challenges are not accepted.
| Requirement | Details |
|---|---|
| PKCE (S256) | Generate cryptographically random code_verifier (43-128 chars), compute SHA256 challenge |
| State parameter | Use state to prevent CSRF. Verify it matches on callback. |
| Nonce | Include nonce in auth request, verify it in id_token |
| HTTPS only | All requests must use HTTPS in production |
| Redirect URI exact match | The redirect_uri must exactly match a registered URI |
| Client secret protection | Never expose client_secret in client-side code. Use it only on the server. |
| Token storage | Store tokens securely (httpOnly cookies or encrypted storage). Never in localStorage. |
| ID Token verification | Verify JWT signature against JWKS, check iss, aud, exp, nonce |
| Refresh token rotation | Refresh tokens are rotated on each use. Always store the latest token. |