Empieza con login OIDC estandar y cuentas self-service familiares. Anade perfiles gestionados solo cuando tu producto necesite dependientes, gestion delegada o continuidad del perfil.
Ejecuta un flujo CLI para crear un tenant, aprovisionar clientes y descargar samples listos para usar con .env prellenado.
npx manage-tuurio-id@1.7.0
Para plantillas server-side con webhooks: primero despliega y despues actualiza la URL del endpoint webhook en la pagina admin del tenant.
MCP Streamable HTTP usa OAuth en el navegador, PKCE, tokens vinculados a recursos y los permisos efectivos del usuario conectado.
codex mcp add tuurio-auth --url https://<tenant>.id.tuurio.com/mcp
codex mcp login tuurio-auth --scopes mcp:connect,mcp:write
Para solo lectura, omite mcp:write. Los hosts con registro dinámico compatible no requieren un cliente creado previamente.
El repositorio publico auth_samples ofrece apps de referencia para asociaciones, escuelas, portales de miembros y herramientas internas. El codigo se queda en GitHub; esta pagina es el punto de entrada para los stacks que tu equipo entrega de verdad.
const { auth } = require('express-openid-connect');
const config = {
authRequired: false,
auth0Logout: true,
secret: 'YOUR_LONG_RANDOM_STRING',
baseURL: 'http://localhost:3000',
clientID: 'CLIENT_ID_FROM_DASHBOARD',
issuerBaseURL: 'https://{your-tenant}.id.tuurio.com',
// Logout requires the ID token from the validated login session.
};
// Add the auth middleware and you are done.
app.use(auth(config));
app.get('/', (req, res) => {
res.send(req.oidc.isAuthenticated() ? 'Signed in as ' + req.oidc.user.name : 'Not signed in');
});
// Logout (OIDC RP-initiated)
app.get('/logout', async (req, res) => {
const idTokenHint = req.oidc.idToken;
if (!idTokenHint) return res.redirect('/');
const issuer = "https://{your-tenant}.id.tuurio.com";
const discovery = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());
const endSession = discovery.end_session_endpoint;
const params = new URLSearchParams({
client_id: 'CLIENT_ID_FROM_DASHBOARD',
id_token_hint: idTokenHint,
post_logout_redirect_uri: 'https://example.com/logout/success'
});
res.redirect(`${endSession}?${params}`);
});
Nota: el secret debe tener al menos 32 caracteres; de lo contrario, la biblioteca puede rechazar el arranque.
end_session_endpoint mediante /.well-known/openid-configuration.
import os
from authlib.integrations.flask_client import OAuth
from flask_session import Session
from redis import Redis
import requests
from urllib.parse import urlencode
# Flask's default session is a signed browser cookie. Use a server-side store
# before retaining the validated ID token for RP-initiated logout.
app.config.update(
SESSION_TYPE="redis",
SESSION_REDIS=Redis.from_url(os.environ["REDIS_URL"]),
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
Session(app)
oauth = OAuth(app)
oauth.register(
name='tuurio',
client_id='CLIENT_ID_FROM_DASHBOARD',
client_secret='CLIENT_SECRET',
server_metadata_url='https://{tenant}.id.tuurio.com/.well-known/openid-configuration',
# Keep the validated ID token in the encrypted server session for logout.
client_kwargs={'scope': 'openid profile email'}
)
@app.route('/login')
def login():
redirect_uri = url_for('callback', _external=True)
return oauth.tuurio.authorize_redirect(redirect_uri)
@app.route('/callback')
def callback():
token = oauth.tuurio.authorize_access_token()
session["tuurio_id_token"] = token["id_token"]
user = token['userinfo']
return f'Hello, {user["name"]}'
@app.route('/logout')
def logout():
id_token_hint = session.pop("tuurio_id_token", None)
if not id_token_hint:
return redirect(url_for("index"))
discovery = requests.get("https://{tenant}.id.tuurio.com/.well-known/openid-configuration").json()
end_session = discovery["end_session_endpoint"]
params = urlencode({
"client_id": "CLIENT_ID_FROM_DASHBOARD",
"id_token_hint": id_token_hint,
"post_logout_redirect_uri": "https://example.com/logout/success",
})
return redirect(f"{end_session}?{params}")
spring:
security:
oauth2:
client:
registration:
tuurio:
client-id: CLIENT_ID_FROM_DASHBOARD
client-secret: CLIENT_SECRET
scope: [openid, profile, email]
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
# Spring's OIDC logout handler supplies the validated ID token hint.
provider:
tuurio:
issuer-uri: https://{your-tenant}.id.tuurio.com
@Bean
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler(
ClientRegistrationRepository registrations) {
var handler = new OidcClientInitiatedLogoutSuccessHandler(registrations);
handler.setPostLogoutRedirectUri("https://example.com/logout/success");
return handler;
}
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http,
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler));
return http.build();
}
// end_session_endpoint via discovery:
// const issuer = "https://{tenant}.id.tuurio.com";
// const discovery = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());
// const endSessionEndpoint = discovery.end_session_endpoint;
import { UserManager } from "oidc-client-ts";
const mgr = new UserManager({
authority: "https://{tenant}.id.tuurio.com",
client_id: "CLIENT_ID_FROM_DASHBOARD",
redirect_uri: "http://localhost:5173/auth/callback",
post_logout_redirect_uri: "http://localhost:5173/",
response_type: "code",
scope: "openid profile email",
automaticSilentRenew: true
});
export const login = () => mgr.signinRedirect();
export const handleCallback = () => mgr.signinRedirectCallback();
// Logout uses end_session_endpoint from discovery
export const logout = () => mgr.signoutRedirect();
import { UserManager } from "oidc-client-ts";
const mgr = new UserManager({
authority: "https://{tenant}.id.tuurio.com",
client_id: "CLIENT_ID_FROM_DASHBOARD",
redirect_uri: "http://localhost:5173/auth/callback",
post_logout_redirect_uri: "http://localhost:5173/",
response_type: "code",
scope: "openid profile email"
});
export const useAuth = () => ({
login: () => mgr.signinRedirect(),
handleCallback: () => mgr.signinRedirectCallback(),
// Logout uses end_session_endpoint from discovery
logout: () => mgr.signoutRedirect()
});
import { AuthConfig, OAuthService } from "angular-oauth2-oidc";
export const authConfig: AuthConfig = {
issuer: "https://{tenant}.id.tuurio.com",
clientId: "CLIENT_ID_FROM_DASHBOARD",
redirectUri: window.location.origin + "/auth/callback",
postLogoutRedirectUri: "http://localhost:5173/",
responseType: "code",
scope: "openid profile email"
};
export const initLogout = async (oauthService: OAuthService) => {
const discovery = await fetch(`${authConfig.issuer}/.well-known/openid-configuration`).then(r => r.json());
oauthService.logoutUrl = discovery.end_session_endpoint;
};
export const logout = (oauthService: OAuthService) => oauthService.logOut();
import NextAuth from "next-auth";
const handler = NextAuth({
providers: [
{
id: "tuurio",
name: "Tuurio",
type: "oidc",
issuer: "https://{tenant}.id.tuurio.com",
clientId: "CLIENT_ID_FROM_DASHBOARD",
clientSecret: "CLIENT_SECRET"
}
],
callbacks: {
async jwt({ token, account }) {
// NextAuth validates the OIDC response before exposing account.id_token.
if (account?.id_token) return { ...token, idToken: account.id_token };
return token;
}
}
});
export { handler as GET, handler as POST };
// app/api/logout/route.ts
import { getToken } from "next-auth/jwt";
import type { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
const idTokenHint = typeof token?.idToken === "string" ? token.idToken : null;
if (!idTokenHint) return Response.redirect(new URL('/', request.url));
const issuer = "https://{tenant}.id.tuurio.com";
const discovery = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());
const endSession = discovery.end_session_endpoint;
const params = new URLSearchParams({
client_id: 'CLIENT_ID_FROM_DASHBOARD',
id_token_hint: idTokenHint,
post_logout_redirect_uri: 'https://example.com/logout/success'
});
return Response.redirect(`${endSession}?${params}`);
}
val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://{tenant}.id.tuurio.com/oauth2/authorize"),
Uri.parse("https://{tenant}.id.tuurio.com/oauth2/token")
)
val request = AuthorizationRequest.Builder(
serviceConfig,
"CLIENT_ID_FROM_DASHBOARD",
ResponseTypeValues.CODE,
Uri.parse("com.example.app:/oauth2redirect")
)
.setScope("openid profile email")
.build()
// val postLogoutRedirectUri = Uri.parse("com.example.app:/logout")
val authService = AuthorizationService(context)
val intent = authService.getAuthorizationRequestIntent(request)
startActivityForResult(intent, RC_AUTH)
// Logout uses the validated ID token retained in AuthState.
AuthorizationServiceConfiguration.fetchFromUrl(
Uri.parse("https://{tenant}.id.tuurio.com/.well-known/openid-configuration")
) { config, _ ->
val idTokenHint = authState.lastTokenResponse?.idToken ?: return@fetchFromUrl
val endSession = EndSessionRequest.Builder(config!!)
.setIdTokenHint(idTokenHint)
.setPostLogoutRedirectUri(Uri.parse("com.example.app:/logout"))
.build()
val endSessionIntent = authService.getEndSessionRequestIntent(endSession)
startActivityForResult(endSessionIntent, RC_LOGOUT)
}
Consejo: AppAuth puede usar la URL de discovery (/.well-known/openid-configuration) para no fijar endpoints manualmente.
let config = OIDServiceConfiguration(
authorizationEndpoint: URL(string: "https://{tenant}.id.tuurio.com/oauth2/authorize")!,
tokenEndpoint: URL(string: "https://{tenant}.id.tuurio.com/oauth2/token")!
)
let request = OIDAuthorizationRequest(
configuration: config,
clientId: "CLIENT_ID_FROM_DASHBOARD",
scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail],
redirectURL: URL(string: "com.example.app:/oauth2redirect")!,
responseType: OIDResponseTypeCode,
additionalParameters: nil
)
// let postLogoutRedirectURL = URL(string: "com.example.app:/logout")!
OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
// Store authState?.lastTokenResponse?.accessToken
}
// Logout uses the validated ID token retained in AuthState.
OIDAuthorizationService.discoverConfiguration(
forIssuer: URL(string: "https://{tenant}.id.tuurio.com")!
) { config, _ in
guard let config = config else { return }
let endSession = OIDEndSessionRequest(
configuration: config,
idTokenHint: authState?.lastTokenResponse?.idToken,
postLogoutRedirectURL: URL(string: "com.example.app:/logout")!,
additionalParameters: nil
)
self.present(OIDAuthorizationService.present(endSession, presenting: self) { _, _ in }, animated: true)
}
Consejo: iOS AppAuth tambien admite discovery para cargar la configuracion automaticamente.
import 'package:flutter_appauth/flutter_appauth.dart';
final appAuth = FlutterAppAuth();
final result = await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
'CLIENT_ID_FROM_DASHBOARD',
'com.example.app:/oauth2redirect',
issuer: 'https://{tenant}.id.tuurio.com',
scopes: ['openid', 'profile', 'email'],
),
);
// Logout (OIDC RP-initiated)
await appAuth.endSession(EndSessionRequest(
idTokenHint: result?.idToken,
postLogoutRedirectUrl: 'com.example.app:/logout',
issuer: 'https://{tenant}.id.tuurio.com',
));
var config = &oauth2.Config{
ClientID: "CLIENT_ID",
ClientSecret: "CLIENT_SECRET",
RedirectURL: "http://localhost:3000/callback",
Scopes: []string{"openid", "profile", "email"},
// Retain the validated ID token in the server-side session for logout.
Endpoint: oauth2.Endpoint{
AuthURL: "https://{tenant}.id.tuurio.com/oauth2/authorize",
TokenURL: "https://{tenant}.id.tuurio.com/oauth2/token",
},
}
// Nutze config.AuthCodeURL(...) und config.Exchange(...)
// Logout (OIDC RP-initiated). idTokenHint comes from validated server session state.
func logoutURL(idTokenHint string) string {
if idTokenHint == "" { return "/" }
resp, _ := http.Get("https://{tenant}.id.tuurio.com/.well-known/openid-configuration")
defer resp.Body.Close()
var discovery struct{ EndSessionEndpoint string `json:"end_session_endpoint"` }
json.NewDecoder(resp.Body).Decode(&discovery)
values := url.Values{
"client_id": {"CLIENT_ID_FROM_DASHBOARD"},
"id_token_hint": {idTokenHint},
"post_logout_redirect_uri": {"https://example.com/logout/success"},
}
return discovery.EndSessionEndpoint + "?" + values.Encode()
}
$provider = new \League\OAuth2\Client\Provider\GenericProvider([
'clientId' => 'CLIENT_ID',
'clientSecret' => 'CLIENT_SECRET',
'redirectUri' => 'https://example.com/callback',
'urlAuthorize' => 'https://{tenant}.id.tuurio.com/oauth2/authorize',
'urlAccessToken' => 'https://{tenant}.id.tuurio.com/oauth2/token',
'urlResourceOwnerDetails' => 'https://{tenant}.id.tuurio.com/userinfo',
// Retain the validated ID token in the server-side PHP session for logout.
]);
// Logout (OIDC RP-initiated)
$discovery = json_decode(file_get_contents("https://{tenant}.id.tuurio.com/.well-known/openid-configuration"), true);
$endSession = $discovery['end_session_endpoint'];
$idTokenHint = $_SESSION['tuurio_id_token'] ?? '';
unset($_SESSION['tuurio_id_token']);
if ($idTokenHint === '') {
header('Location: /');
exit;
}
$params = http_build_query([
'client_id' => 'CLIENT_ID_FROM_DASHBOARD',
'id_token_hint' => $idTokenHint,
'post_logout_redirect_uri' => 'https://example.com/logout/success',
]);
header('Location: ' . $endSession . '?' . $params);
Muchos productos pueden empezar con login self-service. Si mas adelante padres, tutores, personal o miembros responsables deben actuar por otra persona, Tuurio puede modelarlo sin cuentas falsas.
Mantiene los flujos OIDC para credenciales mientras almacenas el perfil real por separado alli donde las operaciones lo necesitan.
Crea primero perfiles de miembros, estudiantes, dependientes o voluntarios que todavia no deben recibir su propio login.
Si un perfil gestionado recibe mas adelante su propio acceso, el mismo perfil mantiene las mismas relaciones e historial.
Define tus propios permisos (por ejemplo inventory:write o reports:view) directamente en el panel de Tuurio.
La autorizacion ocurre en el token. No en tu base de datos.
Spring Security espera authorities con el prefijo SCOPE_ por defecto. Como Tuurio entrega los derechos en el claim permissions, usa un JwtAuthenticationConverter para que @PreAuthorize("hasAuthority('inventory:write')") funcione sin prefijo.
permissions como un array de cadenas.
{
"sub": "user_12345",
"iss": "https://dein-tenant.id.tuurio.com",
"permissions": [
"inventory:write",
"reports:view"
],
"roles": ["ADMIN"]
}
Tuurio tambien entrega claims estandar como email_verified o preferred_username, para que no tengas que duplicarlos en tu base de datos.
@PreAuthorize("hasAuthority('inventory:write')")
@PostMapping("/inventory")
public void updateStock() {
// Tuurio hat's erlaubt!
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.oauth2ResourceServer { oauth2 ->
oauth2.jwt { jwt ->
jwt.jwtAuthenticationConverter(tuurioAuthenticationConverter())
}
}
return http.build()
}
private fun tuurioAuthenticationConverter(): Converter {
val converter = JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter { jwt ->
// Extrahiert das "permissions" Array aus dem Token
val permissions = jwt.getClaim>("permissions") ?: emptyList()
// Mapping zu SimpleGrantedAuthority
permissions.map { SimpleGrantedAuthority(it) }
}
return converter
}
}
Documentacion de referencia para los endpoints de integracion destinados a clientes API orientados a desarrolladores.
Abrir referencia API ->Buenas practicas para almacenamiento de tokens, PKCE y proteccion CSRF.
Leer guia ->