# Atheron for developers: every docs page in full Source: https://developers.aton-network.org/docs. Code examples are tested files, shown once per language. --- # Sign in with Atheron URL: https://developers.aton-network.org/docs What it is, how it works, and the five steps from nothing to a live app. Sign in with Atheron lets people use their Atheron account in your app. It is standard OpenID Connect: the authorization code flow with PKCE, tokens signed with ES256, and a discovery document any OIDC library can read. ## How it works 1. Your app sends the person to our authorize endpoint with your client ID, a redirect address and the scopes you need. 2. They sign in to Atheron, if they are not already, and see a consent screen that lists exactly what you asked for. 3. We send them back to your redirect address with a one-time code. 4. Your server swaps the code for tokens at our token endpoint, proving it is your app. 5. You read who they are from the ID token, or ask the userinfo endpoint for the current answer. ## From nothing to live 1. **Register an app** in the developer portal. You get a test client and a live client straight away. 2. **Build against the test client.** Only your team and up to 25 testers can sign in to it. A tester is somebody you invited who accepted the invitation in their Atheron account. 3. **Add branding and links:** a homepage and your own privacy policy are required to go live. 4. **Verify who you are.** Every owner of the team reaches Bedrock verification, or the business passes its check. 5. **Submit for review.** Once we approve it, anybody with an Atheron account can sign in to your live client. ## What you need to know first - PKCE with S256 is required of every client, including server apps that keep a secret. - Redirect addresses are matched exactly. No wildcards, no prefixes. - People can take back access from their account at any time. Your app hears about it through the consent.revoked webhook. - We never share a password, a date of birth, a document or a payment detail through this service. Start with the [quickstart](/docs/quickstart) in your language, or one for [Next.js](/docs/quickstart/nextjs), [plain Node](/docs/quickstart/node), [Rust](/docs/quickstart/rust) or [mobile](/docs/quickstart/mobile). --- # Quickstart URL: https://developers.aton-network.org/docs/quickstart Sign in with Atheron in your language: settings, the sign-in link, the code swap and who signed in. Pick a language once and every block follows. Four steps, each in cURL and every SDK language. Pick your language in any block or at the top of the page: every block switches, and the docs remember it. Signed in, the samples carry your own test client ID. ## 1. Settings Register an app in the portal first. Its test client works straight away for your team and up to 25 testers. Put these in your server's environment, never in code you ship to a browser. ```bash ATHERON_ISSUER=https://accounts.aton-network.org ATHERON_CLIENT_ID=test-xxxxxxxxxxxxxxxx ATHERON_CLIENT_SECRET=ath_cs_... ATHERON_REDIRECT_URI=http://localhost:3000/auth/callback ``` Add `http://localhost:3000/auth/callback` as a redirect address of your test environment in the portal. Addresses are matched exactly. An https address needs its domain verified first ([Verifying your domain](/docs/domains)). ## 2. Send the person to us Make a fresh PKCE pair, a state and a nonce for this browser, keep them in an HTTP-only cookie or the session, and redirect to the address this gives you. The SDKs do all of it in one call. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## 4. Read who signed in The ID token says who signed in, as of sign-in. Its `sub` is the person's ID for your app alone, and never changes. Ask userinfo when something may have changed since, such as a membership. **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## In your framework The SDKs wire all four steps into your framework's routes and sessions. Pick a framework inside the block where your language has more than one. **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` Next: [protect a page](/docs/guides/sign-in), [refresh and sign out](/docs/tokens), and [go live](/docs/review). --- # Quickstart: Next.js URL: https://developers.aton-network.org/docs/quickstart/nextjs Add Sign in with Atheron to a Next.js app with route handlers and an HTTP-only session cookie. This page is for Next.js. The [quickstart](/docs/quickstart) has every step in every language. This uses openid-client, which reads our discovery document and handles PKCE, state and the token exchange for you. ## 1. Settings ```bash ATHERON_ISSUER=https://accounts.aton-network.org ATHERON_CLIENT_ID=test-xxxxxxxxxxxxxxxx ATHERON_CLIENT_SECRET=ath_cs_... ATHERON_REDIRECT_URI=http://localhost:3000/auth/callback ``` Add `http://localhost:3000/auth/callback` as a redirect address of your test environment in the portal. ## 2. Start the sign-in ```ts // app/auth/start/route.ts import * as client from "openid-client"; import { cookies } from "next/headers"; export async function GET() { const config = await client.discovery(new URL(process.env.ATHERON_ISSUER!), process.env.ATHERON_CLIENT_ID!, process.env.ATHERON_CLIENT_SECRET!); const verifier = client.randomPKCECodeVerifier(); const state = client.randomState(); const nonce = client.randomNonce(); (await cookies()).set("atheron_pending", JSON.stringify({ verifier, state, nonce }), { httpOnly: true, sameSite: "lax", secure: true, path: "/", maxAge: 600 }); const url = client.buildAuthorizationUrl(config, { redirect_uri: process.env.ATHERON_REDIRECT_URI!, scope: "openid profile email", code_challenge: await client.calculatePKCECodeChallenge(verifier), code_challenge_method: "S256", state, nonce, }); return Response.redirect(url.href, 303); } ``` ## 3. Finish it ```ts // app/auth/callback/route.ts import * as client from "openid-client"; import { cookies } from "next/headers"; export async function GET(request: Request) { const jar = await cookies(); const pending = JSON.parse(jar.get("atheron_pending")?.value ?? "{}"); jar.delete("atheron_pending"); const config = await client.discovery(new URL(process.env.ATHERON_ISSUER!), process.env.ATHERON_CLIENT_ID!, process.env.ATHERON_CLIENT_SECRET!); const tokens = await client.authorizationCodeGrant(config, new URL(request.url), { pkceCodeVerifier: pending.verifier, expectedState: pending.state, expectedNonce: pending.nonce, }); const claims = tokens.claims()!; // claims.sub is the person's ID for your app alone, and never changes. Open your own session here. return Response.redirect(new URL("/", request.url), 303); } ``` ## 4. Ask for current details ```ts const info = await client.fetchUserInfo(config, tokens.access_token, claims.sub); ``` The ID token is a snapshot from sign-in. Userinfo answers as of now, so ask it when something may have changed, such as a membership. --- # Quickstart: plain Node URL: https://developers.aton-network.org/docs/quickstart/node The whole flow with nothing but fetch and node:crypto, so you can see every step. No library: every request is visible. Use this to understand the flow, or where you cannot add a dependency. ## Make the PKCE pair and send the person to us ```js import { createHash, randomBytes } from "node:crypto"; const verifier = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); const state = randomBytes(16).toString("base64url"); const url = new URL("https://accounts.aton-network.org/oauth/authorize"); url.search = new URLSearchParams({ response_type: "code", client_id: process.env.ATHERON_CLIENT_ID, redirect_uri: process.env.ATHERON_REDIRECT_URI, scope: "openid profile email", state, code_challenge: challenge, code_challenge_method: "S256", }).toString(); // Keep verifier and state for this browser (an HTTP-only cookie), then redirect to url. ``` ## Swap the code for tokens ```js const answer = await fetch("https://accounts.aton-network.org/oauth/token", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", authorization: "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64"), }, body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri, code_verifier: verifier }), }); const tokens = await answer.json(); // { access_token, token_type, expires_in, scope, id_token, refresh_token? } ``` Check that the `state` you got back is the one you sent before you swap the code. ## Verify the ID token Fetch our keys from `https://accounts.aton-network.org/.well-known/jwks.json`, find the key whose `kid` matches the token header, and check the ES256 signature. Then check `iss` is `https://accounts.aton-network.org`, `aud` is your client ID, `exp` is in the future and `nonce` matches if you sent one. Any JOSE library does this in one call. --- # Quickstart: Rust URL: https://developers.aton-network.org/docs/quickstart/rust Sign in with Atheron from a Rust server using the openidconnect crate. ## Cargo.toml ```toml [dependencies] openidconnect = "4" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } ``` ## Discover, then build the authorize URL ```rust use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreResponseType}; use openidconnect::{AuthenticationFlow, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, PkceCodeChallenge, RedirectUrl, Scope}; let http = reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none()).build()?; let metadata = CoreProviderMetadata::discover_async(IssuerUrl::new("https://accounts.aton-network.org".into())?, &http).await?; let client = CoreClient::from_provider_metadata(metadata, ClientId::new(client_id), Some(ClientSecret::new(secret))) .set_redirect_uri(RedirectUrl::new(redirect_uri)?); let (challenge, verifier) = PkceCodeChallenge::new_random_sha256(); let (url, state, nonce) = client .authorize_url(AuthenticationFlow::::AuthorizationCode, CsrfToken::new_random, Nonce::new_random) .add_scope(Scope::new("profile".into())) .add_scope(Scope::new("email".into())) .set_pkce_challenge(challenge) .url(); ``` ## Swap the code and check the ID token ```rust let tokens = client.exchange_code(AuthorizationCode::new(code))? .set_pkce_verifier(verifier) .request_async(&http).await?; let id_token = tokens.id_token().ok_or("no id token")?; let claims = id_token.claims(&client.id_token_verifier(), &nonce)?; let subject = claims.subject().as_str(); ``` The crate checks the ES256 signature against our published keys, the issuer, the audience, the expiry and the nonce. On Axum or Actix Web, [aton-network-axum](/docs/sdks/rust/axum) and [aton-network-actix](/docs/sdks/rust/actix) do all of this for you, with the session, sign-out and webhooks. --- # Quickstart: mobile and desktop (PKCE) URL: https://developers.aton-network.org/docs/quickstart/mobile Public clients: no secret, PKCE only, the system browser and an app scheme or claimed https link. An app on somebody's phone or computer cannot keep a secret, so register it as a **public** app. It proves itself with PKCE alone, and it has no client secrets. ## Rules for native apps - Use the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android). Never an embedded web view: people cannot see the address bar, and we may refuse it. - Redirect to a claimed https link (Universal Links, App Links) or a private-use scheme in reverse-domain form, such as `com.example.app:/auth`. Register the exact address in the portal. Either needs its domain verified: `com.example.app` is on `app.example.com`, covered by verifying `example.com`. - Send `code_challenge` and `code_challenge_method=S256` on every request, and the matching `code_verifier` when you swap the code. - At the token endpoint send `client_id` in the body and no secret. ## AppAuth AppAuth for iOS and Android follows these rules for you: ```kotlin val config = AuthorizationServiceConfiguration.fetchFromIssuer(Uri.parse("https://accounts.aton-network.org")) { config, _ -> val request = AuthorizationRequest.Builder(config!!, clientId, ResponseTypeValues.CODE, Uri.parse("com.example.app:/auth")) .setScopes("openid", "profile", "offline_access") .build() // AppAuth adds PKCE (S256) itself startActivityForResult(AuthorizationService(this).getAuthorizationRequestIntent(request), RC_AUTH) } ``` Ask for `offline_access` only if the app must keep working while the person is not using it. It gets you a refresh token, and the consent screen says so. --- # Scopes and claims URL: https://developers.aton-network.org/docs/scopes Every scope, what the person is told, what you receive, and which need review. Ask for the least you need. People see every scope on the consent screen, and a sensitive scope makes the live app wait for our review. | Scope | Consent screen says | Claims | Review to go live | |---|---|---|---| | openid | (not shown) | sub, sid | no | | profile | See your name and profile picture | name | no | | email | See your email address | email, email_verified | no | | offline_access | Stay signed in when you are not using it | a refresh token | no | | age | See whether your age is verified and whether you are an adult where you live | age | yes | | membership | See your Atheron membership plan | membership | yes | | wallet | See the crypto wallet addresses linked to your account | wallets | yes | | licences | See which products you have bought | plan | yes | | creator | See whether you are an ATON creator, your creator level and your Aurora Creator plan | creator | yes | An unknown scope, or one your client is not registered for, is refused with `invalid_scope` rather than silently dropped. ## sub `sub` is the ID your app knows the person by. It is made for your app alone: another app gets a different `sub` for the same person, so nobody can join two apps' user lists on it (OpenID Connect pairwise identifiers; our discovery document lists `pairwise` in `subject_types_supported`). It never changes for your app, your test and live clients get the same one, and it is the only thing to key your own account records on. Never key on email: people change it. Apps of one team can share one `sub` per person, for example a web app and a mobile app with one user table. An owner of a verified team (every owner verified, or the business passed its check) switches it on in the app's settings before the app signs anybody in; after that the IDs an app has handed out never change. Webhooks and the events stream use the same `sub` your app was given. Outside apps never receive `sid`. ## age ```json { "age": { "verified": true, "adult": true, "sixteen_or_over": true } } ``` `verified` says an identity check proved their date of birth. `adult` says whether they are an adult where that check placed them, by the age of majority of their province or state; it is null when nothing was verified. Everybody with an account is sixteen or over. You never receive the date of birth or the country. ## membership ```json { "membership": { "tier": "elite", "name": "Aurora", "active": true } } ``` ## wallets ```json { "wallets": [ { "family": "evm", "address": "0x..." }, { "family": "solana", "address": "..." } ] } ``` Addresses the person linked and proved by signature, up to twenty. Families: evm, solana, bitcoin, cosmos, ton, sui, aptos. ## plan and creator See the full shapes in our claims reference; both are read as of the moment you ask userinfo. --- # Tokens and lifetimes URL: https://developers.aton-network.org/docs/tokens What each token is for, how long it lasts and how refresh tokens rotate. | Token | Lifetime | What it is for | |---|---|---| | Authorization code | 60 seconds, once | Swapped at the token endpoint, with the PKCE verifier | | Access token | 15 minutes | Bearer for userinfo; `typ` is `at+jwt`, `aud` is your client ID | | ID token | 5 minutes | Read once, by your app, to learn who signed in | | Refresh token | 90 days unused, 1 year at most | Only with offline_access; each use returns a new one | ## Refresh tokens rotate Every refresh returns a new refresh token, and the old one stops working. If an old one is ever used again, we treat the line as stolen and end every token in it. Keep only the newest, and send one refresh at a time. A refresh token expires after 90 days without use. However often it is used, a line of refresh tokens ends one year after the person signed in: the last refresh before then returns a token that expires on that day, and after it the person signs in again. Plan for that sign-in rather than treating it as an error. ## Never mix them up An ID token is not a credential: do not send it to an API. Userinfo refuses one. An access token says what may be reached, not who signed in to your app. ## When access ends A person can take back your access at any time. From that moment your refresh token stops working, userinfo refuses your access token, and you receive `consent.revoked`. The same happens if we suspend your app. --- # Secrets and certificates URL: https://developers.aton-network.org/docs/credentials Client secrets shown once, rotation without downtime, and certificate credentials with private_key_jwt. A server app proves itself at the token endpoint with a secret or a certificate. Each environment holds up to ten live credentials, each with an expiry of 30 days to two years. ## Secrets A secret starts `ath_cs_`, so secret scanners can find one pushed by mistake. We show it once and keep only a fingerprint and its last four characters. **Rotating without downtime:** make a new secret, deploy it, check the old one's "last used" stops moving, then revoke the old one. ## Certificates (private_key_jwt) Upload the public half of a key pair: an X.509 certificate, a PEM public key or a JWK. EC P-256 (ES256) or RSA of 2048 bits or more (RS256). Never upload the private key; the portal refuses one. At the token endpoint, instead of a secret, send: ```text client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer client_assertion= ``` The JWT is signed with your private key and carries: | Claim | Value | |---|---| | iss, sub | your client ID | | aud | `https://accounts.aton-network.org/oauth/token` | | exp | at most ten minutes ahead | | jti | unique; each one works once | | kid (header) | the thumbprint the portal shows, optional | Send an assertion or a secret, never both. --- # Verifying your domain URL: https://developers.aton-network.org/docs/domains Prove your team owns a domain by a DNS TXT record, a file or a meta tag. Redirects, production and the consent screen depend on it. Sign in with Atheron sends people back only to domains your team has proved it owns. You verify a domain once per team, on the team's **Domains** page (Teams, your team, Manage domains). ## What needs a verified domain | Where | Without a verified domain | With one | |---|---|---| | Test client | Redirects to `http://localhost`, `127.0.0.1` or `[::1]` only | https and app-scheme redirects on that domain too | | Live client | Cannot go live | Every redirect must be on a verified domain | | Consent screen | Named by the address it returns to, with no logo or badge | Your app's name, logo and badges | A domain proved by a DNS record covers itself and every name under it: `example.com` covers `app.example.com`. A domain proved by the file or the meta tag covers that exact host only, so to use several subdomains, prove the apex by DNS or prove each host. An app scheme counts by its reverse-domain name: `com.example.app:/auth` is on `app.example.com`, so a DNS record on `example.com` covers it. Test clients still sign in only your team and the testers who accepted an invite. Going live still needs every owner verified, or the business check passed. ## Three ways to prove it Add the domain to get its token, then use any one of these. **A DNS TXT record (recommended).** On the domain itself, add a TXT record with the value: ```text atheron-verification= ``` **A file.** Serve that same line at `https:///.well-known/atheron-verification.txt`, with a 200 answer and no redirect. **A meta tag.** In the head of `https:///` (before ``, not inside a script or a comment), with a 200 answer and no redirect: ```html ``` Then press **Check now**. A new domain is also checked every hour for its first week, so a DNS record that takes time to spread is picked up on its own. ## Re-checks We check every verified domain once a day. Keep the record, file or tag in place. When our own DNS is not answering, we judge nothing that day. If a check finds no proof, or nothing answers for your domain at all, the domain stays verified and the Domains page shows the date it lapses. After 7 days of failed checks the domain is no longer verified: apps that send people to it stop signing anybody in, and the team's owners get an email. Put the proof back and press Check now to restore it at once. A domain proved by a DNS record that later passes only by the file or the tag keeps covering the names under it for the same 7 days. After that it covers its own host only, so put the DNS record back if your apps use subdomains. ## Rules - Owners and admins add, check and remove domains. Every team member sees them and their tokens. - A team holds up to 20 domains. Two teams may verify the same domain, each with its own token. - Our own domains cannot be verified. - A domain an app still sends people to cannot be removed while no other verified domain covers that redirect. Change the redirect first. - Fetches use https only, check the certificate, follow no redirect, stop after eight seconds in all and read the first 64 KB. --- # Rate limits URL: https://developers.aton-network.org/docs/rate-limits Requests a minute and a day by quota tier, and what happens when you go over. Limits are per app environment, per minute and per day, by bucket. A userinfo call with a sensitive scope (age, membership, wallet, licences, creator) also counts against the smaller sensitive bucket. | Bucket | Free, a minute | Free, a day | Growth, a minute | Growth, a day | |---|---|---|---|---| | token | 120 | 20,000 | 600 | 200,000 | | userinfo | 300 | 50,000 | 1,500 | 500,000 | | userinfo:sensitive | 60 | 10,000 | 300 | 100,000 | | api | 60 | 5,000 | 300 | 50,000 | | sandbox | 30 | 2,000 | 60 | 5,000 | Scale and partner tiers are higher; ask us. Your app's tier and exact limits are on its API keys page. ## Over the limit You get HTTP 429 with `error: rate_limited` and a `Retry-After` header in seconds. Wait that long. Requests over the limit still count, so retrying in a loop keeps you over it. --- # Developer API URL: https://developers.aton-network.org/docs/api Read your app's settings and usage with an API key. Make an API key on your app's API keys page. Keys start `ath_test_` or `ath_live_`, are shown once, last up to a year and carry rights. | Right | Allows | |---|---| | app:read | GET /api/v1/app | | usage:read | GET /api/v1/usage | | events:read | The events stream: GET /api/v1/events, its WebSocket and the GraphQL subscription ([events](/docs/reference/events)) | | webhooks:write | Managing webhooks (coming soon) | | sandbox | Sandbox calls from the MCP server; test keys only | ```bash curl -H "Authorization: Bearer $ATHERON_API_KEY" https://developers.aton-network.org/api/v1/app curl -H "Authorization: Bearer $ATHERON_API_KEY" "https://developers.aton-network.org/api/v1/usage?days=7" ``` Never put a key in a URL or in code you ship to a browser. --- # Sandbox URL: https://developers.aton-network.org/docs/sandbox Try the flow against your test client from the browser before writing code. The [sandbox](/sandbox) runs real calls against your test environment: - Build an authorize URL for your test client with any scopes, with PKCE made for you. - Sign in as yourself or a tester and see the code come back. - Swap it for tokens and read the ID token's claims, decoded. - Call userinfo with the access token. It never uses a live client, and it never shows or stores a secret: you paste your test secret for one call, and it is not kept. --- # Review and going live URL: https://developers.aton-network.org/docs/review What we check, what you need before you submit, and what happens after. ## Before you submit - Every owner of the team has reached Bedrock verification (ID, liveness and a screening check), or the team's business has passed its check. A business check also gives your consent screen a verified badge. - A homepage and your own privacy policy, on https. The policy must say what you do with what you receive from Atheron. - Redirect addresses for the live environment (https, or an app scheme for a public app; never localhost), every one on a domain your team has verified. See [Verifying your domain](/docs/domains). - The scopes the live app needs. Sensitive ones need a reason we can see in your app. - You accepted the developer terms and the data-use policy in force. ## What we check The name, logo, description and links for impersonation of Atheron or anyone else, scam patterns, links that do not belong together, redirect addresses on sites that are not yours, and whether each scope is needed. Automated checks help us; a person decides and writes the reason you see. ## After Approved: your live client signs people in at once, showing what we approved. Changing the name, logo, links or adding a sensitive scope later goes through review again; your test client shows changes at once. Refused: you see why on the Go live page, change what is needed and submit again. ## Suspension and appeals We can suspend an app, or a developer, at once if it harms people. You see the reason in the portal and can appeal there. People who connected a suspended or removed app are told in their account. --- # Button and branding rules URL: https://developers.aton-network.org/docs/branding The Sign in with Atheron button kit, how the button must look, and what you may not do with our name. ## The button Use our button kit, below: one stylesheet, our mark and the labels in five languages. You can load the files from us or serve copies yourself. - Label it **Sign in with Atheron** or **Continue with Atheron**, in the reader's language. The labels are in `/brand/labels.json`: English, French (Canada), Spanish (Spain), German and Urdu. - Put our mark at the start of the button: the left, or the right on a right-to-left page. - Use the dark button (#0a0a0f) on dark pages and the white one on light pages, or the auto theme to follow the reader's colour scheme. - Make it 40 pixels tall. Use 48 on touch screens. Use 32 only in a dense toolbar, beside other 32 pixel buttons. - Give it the same size and weight as any other sign-in buttons on the page. - Do not change the mark's colours or proportions, or add effects to it. On a white button the mark sits on its own dark tile; keep the tile. ## Our name - You may say your app works with Atheron. You may not suggest Atheron made, endorses or runs it. - Your app's name, logo and domain must not use Atheron, ATON or our product names. We refuse names that do. - Never ask people for their Atheron password, recovery codes or authenticator codes. Signing in always happens on our pages. --- # MCP server URL: https://developers.aton-network.org/docs/mcp Connect your AI coding tools to search these docs, generate integration code, check your setup and run sandbox calls. Our MCP server is at `https://developers.aton-network.org/mcp` (Streamable HTTP). Connect your AI tool to it and it can help you add Sign in with Atheron. ## Connecting ```json { "mcpServers": { "atheron": { "type": "http", "url": "https://developers.aton-network.org/mcp", "headers": { "Authorization": "Bearer ath_test_..." } } } } ``` The docs tools work without a key. The tools that read your app or run sandbox calls need a **test** API key with the app:read or sandbox right. A live key is refused for sandbox calls, and the server never sees a live secret. ## Tools | Tool | What it does | |---|---| | search_docs | Finds the docs pages that answer a question | | list_docs | Lists every page, by group | | fetch_doc | Returns one page in full, with its examples in every language | | get_reference | One endpoint, error code or webhook event in full, or the list of them | | get_openapi | The OpenAPI 3.1 description of the whole API | | list_examples | The tested code examples and the languages each exists in | | get_example | One example in your language or framework, falling back to cURL | | get_sdk | How to install and use an SDK, or every SDK and its status | | get_changelog | What changed, newest first | | get_status | Whether the portal, Sign in with Atheron and the developer API are up | | my_app | Your app as registered and, with usage:read, its last seven days; needs app:read | | generate_integration | Writes sign-in code for Next.js, Node, Express, Rust (axum), Python (FastAPI), iOS or Android | | validate_config | Checks redirect addresses, scopes and PKCE settings, yours or ones you paste | | explain_error | Says what an error code means here and how to fix it | | sandbox_authorize_url | Builds an authorize URL with PKCE for your test client | | sandbox_userinfo | Calls userinfo with a test access token | | sandbox_discovery | Reads our discovery document | Rate limited by the sandbox bucket of your app's tier, or by address without a key. --- # Reporting a vulnerability URL: https://developers.aton-network.org/docs/security How to tell us about a security problem in Sign in with Atheron or any Atheron service, and what we promise in return. If you find a security problem in Sign in with Atheron, the developer platform or any Atheron service, tell us first and give us time to fix it. ## How to report Email **security@aton-network.org** with what you found, how to reproduce it, and what an attacker could do with it. Every developer-facing host also lists this in `/.well-known/security.txt`. ## What we promise - We reply within two working days, and keep you told until it is fixed. - We will not take legal action against you, or ask anybody else to, for research done in good faith under this page. - We credit you here once it is fixed, if you want us to. ## What good faith means - Test only with accounts and apps you own, or with permission from their owner. Use your own test app and testers. - Do not read, change or keep anybody else's data beyond the least needed to show the problem, and delete it after. - Do not degrade the service: no denial of service, no high-volume automated scanning, no spam, no social engineering of our staff or members, no physical attacks. - Do not tell anybody else until we have fixed it, or until 90 days have passed since you told us, whichever comes first. ## In scope accounts.aton-network.org (the OpenID Connect provider), developers.aton-network.org (the portal, the developer API and the MCP server), my.aton-network.org, and the SDKs (atheron-sdk-ts, atheron-sdk-rs). ## Thanks Nobody yet. Be the first. --- # Changelog URL: https://developers.aton-network.org/docs/changelog What changed, newest first. ## 2026-10-30 - Resend past webhook deliveries from the Webhooks tab (one, or every one that gave up) or with `atheron webhooks replay`. The same body and event ID, signed as it goes, tried once; 25 at a time and 60 an hour for an app; in the app's Activity. `atheron webhooks deliveries` lists them. - The [GraphQL playground](/graphql) in English, French, Spanish, German and Urdu. - GraphQL helpers in the SDKs: typed queries, persisted queries and partial answers in every SDK, with subscriptions that resume in JavaScript and TypeScript and in Python. `atheron api graphql` runs one request from your terminal. ## 2026-10-15 - Code tabs on every docs block: cURL, TypeScript, Python, Go, Java, Ruby, PHP, C#, Rust and Elixir. Pick one anywhere and every block on the page switches; the docs remember it, and so does your account when you are signed in. - A [quickstart](/docs/quickstart) with every step in every language. Signed in, the samples carry your own test client ID. - Try it: run the discovery document, the signing keys, `GET /api/v1/app` and `GET /api/v1/usage` from the page with a test key, and see the real answer beside the code. - Ask Atlas about any block: our AI assistant explains it step by step, with the pages it used. - Docs rebuilt: a page for every endpoint and webhook event, every error with its cause and fix, guides and tutorials, an [OpenAPI description](/openapi.json), and code in cURL, Node.js and Rust that follows the language you pick. - Ask the docs: an assistant on every docs page that answers from these pages, with links, and hands over to a person. - Chat with us from any page; the Developer support team follows up from 8 AM to 10 PM Pacific. - The MCP server serves the whole reference, every example by language, SDK usage, the changelog, status and your app. - [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt) for assistants. - Developer portal opens: register apps, teams and roles, test and live environments, secrets and certificates, testers, review. - Testers accept an invitation in their Atheron account before they can sign in to your test client; they see your app, your team and what it asks for. - A line of refresh tokens lasts one year at most from the sign-in, and 90 days without use. - New scopes: age, membership, wallet. - private_key_jwt client authentication. - Every app gets its own `sub` for a person (pairwise), so apps cannot match their users with each other. Apps of one verified team can share one. See [sub](/docs/scopes#sub). - API keys, rate limits by quota tier, usage charts, webhooks. - MCP server and sandbox. - The Sign in with Atheron button kit: one stylesheet, our mark, dark, light and auto themes, three sizes, labels in five languages ([branding](/docs/branding)). - Uptime on the [status page](https://progress.aton-network.org/status): the developer portal, Sign in with Atheron (OAuth), and the developer API with the MCP server, each on its own line. --- # Tutorial: sign-in from start to finish URL: https://developers.aton-network.org/docs/guides/sign-in Register an app, send somebody to sign in, handle the callback, check the ID token, keep a session and sign out. This walks through a whole sign-in on a server app. Pick your language in any code block and every block follows. Plan on about twenty minutes. ## 1. Register an app Sign in to the [developer portal](/apps) and choose **New app**. Pick **Confidential** for a server app that can keep a secret, or **Public** for a mobile, desktop or browser app. You get a test client and a live client at once. On the app's **Sign-in** page add your redirect address for the test client, for example `http://localhost:3000/callback`. Test clients may use localhost; live clients may not. Any https address, in test or live, must be on a domain your team verified on its Domains page ([Verifying your domain](/docs/domains)). On **Credentials**, make a secret. It starts `ath_cs_` and is shown once: put it in your server's environment as `ATHERON_CLIENT_SECRET`, with the client ID as `ATHERON_CLIENT_ID`. ## 2. Send somebody to sign in When somebody presses Sign in with Atheron, make a PKCE pair, a state and a nonce, keep them in an HTTP-only cookie for this browser, and redirect to the authorize address. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## 4. Check the ID token Before you trust the ID token, check its signature against our [signing keys](/docs/reference/jwks) (ES256 only), that `iss` is `https://accounts.aton-network.org`, that `aud` is your client ID, that it has not expired, and that `nonce` is the one you kept. A JOSE library does all of this in one call. ## 5. Keep your own session Key your user record on `sub`. It is your app's own ID for the person (another app sees a different one) and it never changes. Never key on email: people change it. Then set your own session cookie and forget the tokens unless you need userinfo later. **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## 6. Sign out Ending your session is enough for most apps. If you kept a refresh token, revoke it so it can never be used again. **cURL** ```bash # End a refresh token and every token in its line. Always answers 200 once your app is authenticated. curl https://accounts.aton-network.org/oauth/revoke \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d token="$REFRESH_TOKEN" \ -d token_type_hint=refresh_token ``` **TypeScript** ```ts // When somebody signs out of your app for good, end their refresh token. import { ISSUER } from "@aton-network/sdk"; export async function revokeRefreshToken(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/revoke`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token: refreshToken, token_type_hint: "refresh_token" }), }); if (!answer.ok) throw new Error(`revoke answered ${answer.status}`); } ``` **Python** ```python # When somebody disconnects your app: revoke their refresh token, then delete it. import os from aton_network import Client def revoke_refresh_token(refresh_token: str) -> None: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) client.revoke_token(refresh_token, "refresh_token") ``` **Rust** ```rust // Revoke a refresh token when somebody signs out or disconnects your app. use aton_network::{Client, Error}; pub async fn revoke_refresh_token(refresh_token: &str) -> Result<(), Error> { Client::from_env().revoke_token(refresh_token, Some("refresh_token")).await } ``` **Go** ```go // Revoke a refresh token when somebody signs out or disconnects your app. package revoke import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RevokeRefreshToken(ctx context.Context, refreshToken string) error { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RevokeToken(ctx, refreshToken, "refresh_token") } ``` **Java** ```java // Revoke a refresh token when somebody signs out or disconnects your app. package examples.revoke.java; import org.atonnetwork.atheron.AtheronClient; public class RevokeRefreshToken { public static void revokeRefreshToken(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); client.revokeToken(refreshToken, "refresh_token"); } } ``` **Kotlin** ```kotlin // Revoke a refresh token when somebody signs out or disconnects your app. package examples.revoke.kotlin import org.atonnetwork.atheron.AtheronClient fun revokeRefreshToken(refreshToken: String) { AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).revokeToken(refreshToken, "refresh_token") } ``` **C#** ```csharp // Revoke a refresh token when somebody signs out or disconnects your app. using AtonNetwork; namespace Examples.Revoke; public static class RevokeRefreshToken { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RevokeTokenAsync(refreshToken, "refresh_token"); } } ``` **PHP** ```php revokeToken($refreshToken, 'refresh_token'); } ``` **Ruby** ```ruby # When somebody signs out of your app for good, end their refresh token. require "aton_network" def revoke_refresh_token(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.revoke_token(refresh_token, "refresh_token") end ``` **Elixir** ```elixir # When somebody signs out of your app for good, end their refresh token. defmodule MyApp.Revoke do def revoke_refresh_token(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.revoke_token(refresh_token, "refresh_token") end end ``` ## Next - [Webhooks tutorial](/docs/guides/webhooks): hear when somebody takes back your access. - [Going to production](/docs/guides/production): what we check before your live client opens. --- # Tutorial: webhooks URL: https://developers.aton-network.org/docs/guides/webhooks Add a webhook, check its signature, handle each event once, and test it before you go live. Webhooks tell your server when something happens that you did not cause: somebody signed in, took back your access, or changed their plan. ## 1. Add an address On your app's **Webhooks** page, add an https address for the test environment and choose the events you want. The signing secret starts `whsec_` and is shown once: keep it as `ATHERON_WEBHOOK_SECRET`. Your address must be public. We never deliver to a private or loopback address, and never follow a redirect. ## 2. Check every delivery Read the raw body before any JSON parser touches it, and check `Atheron-Signature` against it. **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## 3. Handle each event once Every delivery carries `Atheron-Event-Id`. Keep the ids you handled for a day and answer 2xx to a repeat without doing the work again. A delivery that does not get a 2xx within ten seconds is tried again, up to seven more times over nearly two days. ## 4. Act on consent.revoked When somebody takes back your access you get [consent.revoked](/docs/webhooks/consent.revoked). Delete the refresh token you hold for that `sub` and stop using what you read, as the data-use policy says. ## 5. Test it Press **Send a test** on the Webhooks page. You get a [ping](/docs/webhooks/ping) at once, and the page shows each attempt with its status. If your endpoint missed some, press **Resend** beside a delivery, or **Resend the ones that gave up**, once it is back. The full list of events and fields is in [Webhooks](/docs/webhooks). --- # Going to production URL: https://developers.aton-network.org/docs/guides/production The checklist before you submit your app for review, and what changes once it is live. ## The checklist - **Redirect addresses for the live client:** https only, or an app scheme for a public app, each on a domain your team verified. Never localhost. - **Least scope:** ask for only what the app uses. Each sensitive scope (age, membership, wallet, licences, creator) needs a reason we can see in your app. - **A homepage and a privacy policy** on https, the policy saying what you do with what you receive from Atheron. - **Your name and logo** are yours, and do not use Atheron, ATON or our product names. - **Verification:** every owner of the team has reached Bedrock verification, or the team's business has passed its check. A business check also gives your consent screen a verified badge. - **Secrets** are in your server's environment, never in a repository or a browser. Rotate any that were ever shared. - **Webhooks** for the live environment, if you use them, with signatures checked. - **Rate limits:** read your tier's limits on the app's API keys page and back off on 429. - **Errors:** every error code you can meet has a path in your app. See [Errors](/docs/errors). ## Submit On the app's **Go live** page, submit for review. A person reviews every app and writes the reason you see. See [Review and going live](/docs/review) for what we check. ## Once it is live Anybody with an Atheron account can sign in. Changing the name, logo or links, or adding a sensitive scope, goes through review again; your test client shows changes at once, so build them there first. --- # The verified badge URL: https://developers.aton-network.org/docs/guides/verified-badge What the verified badge on the consent screen means and how your team earns it. The consent screen shows a verified badge beside your app's name when the business behind your team has passed our business check. People see it before they agree, so it is worth having for any app run by a company. ## How to get it 1. Open your team's page in the portal. 2. Start the business check. Only an owner can. 3. Follow the check with our verification partner: the company's registration and its officers. 4. It passes when an owner who reached Bedrock verification is among the officers the registry names. Otherwise a person on our side decides. The badge appears on every app the team owns. If the check fails you see why, and can start it again once the cause is fixed. ## What it does not mean The badge says who is behind the app. It does not mean Atheron made, runs or endorses the app. --- # Moving from another sign-in provider URL: https://developers.aton-network.org/docs/guides/migrating Add Sign in with Atheron beside the provider you use today, link existing accounts safely, and switch over. Sign in with Atheron is standard OpenID Connect, so the library you use for another provider (Google, Auth0, Okta and the like) already speaks it. Moving is mostly configuration and one careful step: linking the accounts you already have. ## 1. Add Atheron beside what you have Register your app, point your OIDC library at our [discovery document](/docs/reference/discovery), and add a Sign in with Atheron button next to your current one. Keep both until you are ready. ## 2. Link accounts safely The first time somebody signs in with Atheron, you will not have their `sub` yet. Link it to an existing account only when one of these holds: - They are already signed in to your app with the old provider when they connect Atheron. This is the safest way. - The email we send has `email_verified` true and it matches an account whose email you verified yourself. Never link on an unverified email. Once linked, store `sub` on the account and key on it from then on. ## 3. Switch over When most people have linked, make Atheron the main button. Keep the old provider for anyone who has not linked yet, and tell them how. ## Upgrading an SDK Our SDKs follow semantic versioning: a patch fixes, a minor adds, a major can break. Every change is in the [changelog](/docs/changelog). --- # Troubleshooting URL: https://developers.aton-network.org/docs/guides/troubleshooting Step by step: sign-in that never comes back, codes that will not swap, userinfo refusing tokens, and webhooks that do not arrive. Work down each list until one step fixes it. If none does, ask in the chat with the error code and the time it happened. ## The person never comes back to my app 1. Does our page show an error instead of signing them in? An unknown client, an unregistered redirect address or a repeated parameter is shown there and never sent back. Check the redirect address matches one you registered **exactly**, scheme, host, port and path. 2. Is it the test client? Only your team and testers who accepted your invitation can sign in. Check the **Testers** page: an invitation still waiting does not count. 3. Is it the live client? It signs in nobody until the review is approved. 4. Did you send `prompt=none`? Then a missing sign-in or consent comes back as an error, not a page. ## The code will not swap (invalid_grant) 1. Swap it within 60 seconds, and only once. A retry after a timeout fails because the first attempt used it. 2. Send the same `redirect_uri` the authorize request had. 3. Send the `code_verifier` whose challenge you sent, from the same browser's cookie. 4. Make sure you swap with the client the code was issued to: test codes on the test client. ## invalid_client at the token endpoint 1. Is the secret the current one, and not expired? Check **Credentials**. 2. A public app sends no secret at all. 3. With private_key_jwt: `aud` is the token endpoint, `exp` at most ten minutes ahead, and each `jti` new. ## Userinfo answers invalid_token 1. Send the access token, not the ID token. 2. Access tokens last 15 minutes: refresh, or sign them in again. 3. The person may have taken back access. You will have had [consent.revoked](/docs/webhooks/consent.revoked). ## Webhooks do not arrive 1. Is the address public and https? We never deliver to private or loopback addresses, and never follow redirects. 2. Do you answer 2xx within ten seconds? Slow handlers should queue the work and answer at once. 3. Press **Send a test** and read the attempt's status on the Webhooks page. 4. Check the signature against the **raw** body. A parsed and re-serialised body never matches. ## 429 rate_limited Wait the `Retry-After` seconds. Requests over the limit still count, so a tight retry loop keeps you over it. Your limits are on the app's API keys page. --- # Getting help URL: https://developers.aton-network.org/docs/support Ask the docs, chat with us, check the status page, or report a vulnerability. ## Ask the docs The assistant on every docs page answers from these docs, with links to the pages it used. If you are signed in and say it may, it also reads your apps (names, states, review answers, usage counts and failing webhooks) to answer about them. It never sees a secret. ## Chat with us Use the chat button at the bottom of any page. Our assistant answers right away, and a person on our developer support team follows up from 8 AM to 10 PM Pacific. Out of those hours, leave a message and we answer by email. ## Status Uptime for the developer portal, Sign in with Atheron and the developer API is on the [status page](https://progress.aton-network.org/status). ## Security Found a vulnerability? Follow [Reporting a vulnerability](/docs/security). Please do not post it in the chat. --- # ATON Kestrel API URL: https://developers.aton-network.org/docs/kestrel Signals with explanations, the sentiment view, the Kestrel Eye, backtests and paper trading from our own AI model family, by the seat. ATON Kestrel is our own AI model family, built from the ground up for trading: a market model trained on years of exchange data makes the decisions, and a language model explains each one in plain words. The Kestrel API gives your app the same answers our trading app uses. ## What you can build - **Signals and explanations:** for each market, strategy and timeframe, which way the model leans, the expected return after fees in basis points, the risk, and the reasons, pinned to the ATON Kestrel version you choose. REST, history and a live stream. - **The sentiment view:** mood and urgency for each coin, the veto that pauses a bot on urgent bad news, and the alerts behind it. - **The Kestrel Eye feed:** every market at once, with the opportunities a gated strategy sees. - **Backtests as a service:** your question replayed on our desk with real fees, always against simply holding the market. - **A trading app like ours:** paper trading, bots and portfolios for the people who sign in to your app with Atheron. Live orders unlock when live trading opens for everyone. ## How it works 1. Sign in to this portal with your Atheron members account. You need one to use the portal at all. 2. Make a team, then buy a seat for it on the [Kestrel API](/kestrel) page. Until ATON Kestrel v0.1.0 is published, join the waitlist there. 3. Make a live API key with the **kestrel** right on one of your apps, and put it on the seat. 4. Call the endpoints with the key as `Authorization: Bearer`. A test key answers the same shapes and is never counted. The Kestrel API is a tool, not advice. Nothing it answers is a recommendation or a promise of returns. --- # Kestrel API quickstart URL: https://developers.aton-network.org/docs/kestrel/quickstart Your first signal in three requests. ## 1. A key Make an API key with the **kestrel** right on your app's API keys page. Use a test key while you build: it is never counted, and `Kestrel-Test-Tier: builder`, `pro` or `studio` shows you what each seat sees. ## 2. The versions ```sh curl -s https://developers.aton-network.org/api/kestrel/v1/versions -H "Authorization: Bearer $ATHERON_API_KEY" ``` ## 3. A signal **cURL** ```bash # Which way ATON Kestrel leans on BTCUSDT now, pinned to a version. The answer's headers carry the month's usage. curl -s -D - "https://developers.aton-network.org/api/kestrel/v1/signals?symbol=BTCUSDT" \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Kestrel-Version: v0.1.0" ``` **TypeScript** ```ts // Which way ATON Kestrel leans on a market now, pinned to a version, and the month's calls so far. export async function kestrelLeaning(apiKey: string, symbol = "BTCUSDT"): Promise<{ leaning: string[]; callsUsed: number | null }> { const answer = await fetch(`https://developers.aton-network.org/api/kestrel/v1/signals?symbol=${symbol}`, { headers: { authorization: `Bearer ${apiKey}`, "kestrel-version": "v0.1.0" }, }); if (!answer.ok) throw new Error(`the Kestrel API answered ${answer.status}`); const { data } = (await answer.json()) as { data: { action: string; gate: { passed: boolean } }[] }; const used = answer.headers.get("kestrel-calls-used"); return { leaning: data.filter((s) => s.action !== "none" && s.gate.passed).map((s) => s.action), callsUsed: used === null ? null : Number(used) }; } ``` **Python** ```python # Which way ATON Kestrel leans on a market now, after fees, and what the month's calls stand at. from aton_network import AtheronKestrel def kestrel_view(api_key: str, symbol: str = "BTCUSDT") -> dict: kestrel = AtheronKestrel(api_key, version="v0.1.0") signals = kestrel.signals(symbol=symbol)["data"] leaning = [s for s in signals if s["action"] != "none" and s["gate"]["passed"]] return {"signals": len(signals), "leaning": [s["action"] for s in leaning], "calls_used": kestrel.usage["used"]} ``` **Rust** ```rust // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. use aton_network::{Error, Kestrel}; use serde_json::Value; pub async fn kestrel_leaning(api_key: &str, symbol: &str) -> Result<(Vec, Option), Error> { let kestrel = Kestrel::new(api_key)?.with_version("v0.1.0")?; let answer: Value = kestrel.signals(Some(symbol), None, None).await?; let leaning = answer["data"] .as_array() .map(|all| all.iter().filter(|s| s["action"] != "none" && s["gate"]["passed"] == true).filter_map(|s| s["action"].as_str().map(str::to_string)).collect()) .unwrap_or_default(); Ok((leaning, kestrel.usage().used)) } ``` **Go** ```go // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. package kestrelsignals import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Leaning(ctx context.Context, apiKey, symbol string) ([]string, *int64, error) { kestrel, err := atheron.NewKestrel(apiKey, atheron.WithKestrelVersion("v0.1.0")) if err != nil { return nil, nil, err } var answer struct { Data []struct { Action string `json:"action"` Gate struct { Passed bool `json:"passed"` } `json:"gate"` } `json:"data"` } if err := kestrel.Signals(ctx, atheron.SignalsParams{Symbol: symbol}, &answer); err != nil { return nil, nil, err } leaning := []string{} for _, s := range answer.Data { if s.Action != "none" && s.Gate.Passed { leaning = append(leaning, s.Action) } } return leaning, kestrel.Usage.Used, nil } ``` **Java** ```java // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. package examples.kestrelsignals.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.Kestrel; public class KestrelLeaning { @SuppressWarnings("unchecked") public static List kestrelLeaning(String apiKey, String symbol) { Kestrel kestrel = new Kestrel(apiKey, "v0.1.0"); Map answer = (Map) kestrel.signals(symbol, null, null); return ((List>) answer.get("data")).stream() .filter(s -> !"none".equals(s.get("action")) && Boolean.TRUE.equals(((Map) s.get("gate")).get("passed"))) .map(s -> (String) s.get("action")) .toList(); } } ``` **Kotlin** ```kotlin // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. package examples.kestrelsignals.kotlin import org.atonnetwork.atheron.Kestrel @Suppress("UNCHECKED_CAST") fun kestrelLeaning(apiKey: String, symbol: String): List { val kestrel = Kestrel(apiKey, "v0.1.0") val answer = kestrel.signals(symbol, null, null) as Map return (answer["data"] as List>) .filter { it["action"] != "none" && (it["gate"] as Map)["passed"] == true } .map { it["action"] as String } } ``` **C#** ```csharp // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. using AtonNetwork; namespace Examples.KestrelSignals; public static class KestrelLeaning { public static async Task<(List Leaning, long? CallsUsed)> RunAsync(string apiKey, string symbol = "BTCUSDT") { var kestrel = new Kestrel(apiKey, version: "v0.1.0"); var answer = await kestrel.SignalsAsync(symbol: symbol); var leaning = answer?["data"]?.AsArray() .Where(s => (string?)s?["action"] != "none" && (bool?)s?["gate"]?["passed"] == true) .Select(s => (string)s!["action"]!) .ToList() ?? []; return (leaning, kestrel.Usage.Used); } } ``` **PHP** ```php signals(symbol: $symbol)['data']; $leaning = array_values(array_map(fn (array $s) => $s['action'], array_filter($signals, fn (array $s) => $s['action'] !== 'none' && $s['gate']['passed']))); return ['leaning' => $leaning, 'callsUsed' => $kestrel->usage['used']]; } ``` **Ruby** ```ruby # Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. require "aton_network" def kestrel_leaning(api_key, symbol = "BTCUSDT") kestrel = AtonNetwork::Kestrel.new(api_key, version: "v0.1.0") signals = kestrel.signals(symbol: symbol)["data"] { leaning: signals.select { |s| s["action"] != "none" && s["gate"]["passed"] }.map { |s| s["action"] }, calls_used: kestrel.usage["used"] } end ``` **Elixir** ```elixir # Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. defmodule MyApp.Kestrel do def leaning(api_key, symbol \\ "BTCUSDT") do kestrel = AtonNetwork.Kestrel.new(api_key, version: "v0.1.0") with {:ok, %{"data" => signals}, usage} <- AtonNetwork.Kestrel.signals(kestrel, symbol: symbol) do leaning = for s <- signals, s["action"] != "none" and s["gate"]["passed"], do: s["action"] {:ok, leaning, usage["used"]} end end end ``` Each answer carries `Kestrel-Version`, `Kestrel-Weight`, `Kestrel-Calls-Used` and `Kestrel-Calls-Allowed`, so you always know where the month stands. --- # Seats, prices and discounts URL: https://developers.aton-network.org/docs/kestrel/pricing Builder, Pro, Studio and Enterprise; paper-mode prices; plan discounts; overage. Prices are in Canadian dollars for one seat for a month, before tax. | Seat | A month | Calls a month | Extra calls, per 1,000 | What it adds | |---|---|---|---|---| | Builder | C$39 | 25,000 | C$1.50 | Signals one minute delayed, and the sentiment view. | | Pro | C$149 | 250,000 | C$1 | Real-time signals with explanations, webhooks and streams, and the Kestrel Eye feed. | | Studio | C$499 | 2,000,000 | C$0.60 | Priority, backtests as a service and higher rate limits. | | Enterprise | Quoted | Agreed | Agreed | Custom calls, support and terms. Ask us in the chat. | ## While trading is on paper Until live trading opens for everyone, **Builder is free** with its 25,000 calls (it stops at the allowance, as there is no card behind it), and **Pro and Studio are half price with double the calls**. When live trading opens, normal prices apply from each seat's next month, and a seat that was paid for while trading was on paper keeps **20% off for 12 months** as a founding builder. ## Discounts on your members plan - **Aurora:** 15% off every seat. - **Borealis:** 30% off every seat. - **Superfluid:** one Builder seat free for life, and 30% off the rest. Discounts do not stack with each other: a seat takes the larger of its plan discount and the founding builder's 20%. ## Extra calls A paid seat keeps answering past its allowance and bills the extra calls at its rate per 1,000, at the end of the month. You get an email at 80% and at 100%. --- # Versions and weights URL: https://developers.aton-network.org/docs/kestrel/versions Pin a version; each call counts with its version's weight. Every call is answered by one published ATON Kestrel version. Pin it with the `Kestrel-Version` header (or `?version=`); without one you get the newest. Versions read like the sites' own: ATON Kestrel ALPHA v0.1.0, then BETA, then no label at full launch. ## Weights A call counts with its version's weight, like the per-model prices of other AI APIs: - every older published version counts **1x**; - the newest major version counts **up to 1.5x**, set by a founder when it is published; - a patch update never changes the weight. When a new major version is published, the previous one falls to 1x by itself. The weights now are on the [Kestrel API](/kestrel) page and at `GET /api/kestrel/v1/versions`. ## What changes between versions Patch versions retrain on new data with the same inputs and ride along inside the version you pinned. Minor versions add indicators or strategies; major versions are a new model design. We tell you before a minor or major version is published, and the last three minor versions stay available, with 90 days' notice before one retires. --- # Kestrel API reference URL: https://developers.aton-network.org/docs/kestrel/reference Every endpoint, what it costs and the lowest seat that may call it. Base address: `https://developers.aton-network.org`. Every request sends `Authorization: Bearer `; every answer is JSON. | Endpoint | Counts as | From | |---|---|---| | `GET /api/kestrel/v1/signals` | 1 call | Builder | | `GET /api/kestrel/v1/signals/{symbol}` | 1 call | Builder | | `GET /api/kestrel/v1/signals/{symbol}/history` | 5 calls | Builder | | `GET /api/kestrel/v1/signals/stream` | 1 call | Pro | | `GET /api/kestrel/v1/sentiment` | 1 call | Builder | | `GET /api/kestrel/v1/sentiment/history` | 5 calls | Builder | | `GET /api/kestrel/v1/eye` | 1 call | Pro | | `POST /api/kestrel/v1/backtests` | 100 calls | Studio | | `GET /api/kestrel/v1/backtests/{id}` | 1 call | Studio | | `GET /api/kestrel/v1/versions` | 1 call | Builder | | `POST /api/kestrel/v1/paper/orders` | 1 call | Builder | | `GET /api/kestrel/v1/paper/bots` | 1 call | Builder | | `GET /api/kestrel/v1/paper/portfolio` | 1 call | Builder | | `POST /api/kestrel/v1/live/orders` | 1 call | Builder | ## Signals `GET /api/kestrel/v1/signals` answers the newest signal of each market (`?symbol=` for one, `?limit=` up to 500). `GET /api/kestrel/v1/signals/{symbol}` answers every strategy and timeframe of one market, and `/history?from=&to=` its past signals, 30 days kept. ```json { "object": "signal", "version": "v0.1.0", "venue": "bitunix", "segment": "usdt-m", "symbol": "BTCUSDT", "strategy": "trend", "interval": "1h", "at": "2026-10-20T14:00:00.000Z", "valid_until": "2026-10-20T15:00:00.000Z", "action": "none", "direction": { "down": 0.21, "flat": 0.55, "up": 0.24 }, "net_bps": { "long": -3.1, "short": -14.0 }, "cost_bps": 15.2, "risk": { "sortino_long": -0.05, "sortino_short": -0.4, "mae_long": 0.012, "mae_short": 0.018 }, "gate": { "passed": false }, "readers": { "agree": false, "sentiment": "neutral", "why": "model-no-edge" }, "explanations": [{ "code": "cost_exceeds_edge", "weight": 0.62, "value": 15.2, "unit": "bps", "facts": { "expected_bps": 4.1 } }] } ``` `net_bps` is the expected return per trade after fees, spread, slippage and funding. A strategy that has not passed its backtest gate shows `gate.passed: false`: research, not switched on. Builder seats read signals a minute behind and without `explanations`. ## Streams `GET /api/kestrel/v1/signals/stream` (Pro and up) is server-sent events: one `signal` event for each new signal. Each batch sent counts as one call. A stream lasts 15 minutes; reconnect with `Last-Event-ID` to carry on. ## Sentiment `GET /api/kestrel/v1/sentiment` answers mood (level from `-1` to `1`, speed, how unusual), urgency and the veto for each coin, with alerts by kind and state. `/history?from=&to=` answers past views, 400 days kept. Headlines are not republished. ## The Kestrel Eye `GET /api/kestrel/v1/eye` (Pro and up) answers every market's newest signal, the opportunities (a gated signal that says long or short) and the sentiment weather in one call. ## Backtests `POST /api/kestrel/v1/backtests` (Studio and up) with `venue`, `symbol`, `strategy`, `interval`, `from` and `to` (three years at most) queues a backtest; it counts as 100 calls and answers `202` with its `id`. `GET /api/kestrel/v1/backtests/{id}` answers its state and, when done, `net_bps`, `holding_bps` and `excess_bps`: every result is measured against simply holding the market, after fees. Five at once for an app. ## Paper trading and live orders `POST /api/kestrel/v1/paper/orders`, `GET /api/kestrel/v1/paper/bots` and `GET /api/kestrel/v1/paper/portfolio` act for a person who signed in to your app with Atheron and granted the trading scope: send their access token as `Atheron-User-Token`. Nothing reaches an exchange. `POST /api/kestrel/v1/live/orders` answers `403 live_trading_closed` until live trading opens for everyone; then it places orders with that person's own exchange key, locked to our engine's address, through our engine's routing and guardrails. --- # Kestrel API errors and limits URL: https://developers.aton-network.org/docs/kestrel/errors Every error code, what caused it and what to do; rate limits by seat. Errors have the shape `{ "error": "...", "error_description": "..." }`, the same as the rest of the developer API. | Code | Status | Cause | What to do | |---|---|---|---| | `invalid_key` | 401 | No key, not ours, expired or revoked. | Send a key with the kestrel right. | | `missing_right` | 403 | The key has no kestrel right. | Make a key with it. | | `no_seat` | 402 | A live key that is not on an active seat. | Put the key on a seat on the Kestrel API page. | | `tier_too_low` | 403 | The endpoint needs a higher seat. It was not counted. | Upgrade the seat. | | `allowance_used` | 429 | A free seat used its month's calls. | Wait for next month, or buy a paid seat. | | `invalid_version` | 400 | `Kestrel-Version` is not `v..`. | Send a version from `/versions`. | | `unknown_version` | 404 | Not published, or retired. | Pin a published version. | | `not_published` | 503 | No version is published yet. | Join the waitlist. | | `rate_limited` | 429 | Over your app's per-minute quota. | Wait the `Retry-After` seconds. | | `user_token_needed` | 401 | A trading call without the person's token. | Send `Atheron-User-Token`. | | `live_trading_closed` | 403 | Live orders before live trading opens. | Use the paper endpoints. | | `not_connected` | 503 | Trading through the API is not connected yet. | Try again later. | | `too_many_backtests` | 429 | Five already waiting or running. | Wait for one to finish. | ## Rate limits Requests per minute: Builder 60, Pro 300, Studio 1,200, Enterprise agreed. Over the limit, requests are refused with `Retry-After` and still count against your quota, so a retry loop keeps you over it. --- # Kestrel API in the SDKs URL: https://developers.aton-network.org/docs/kestrel/sdks The Kestrel API in every Atheron SDK, generated from the same OpenAPI document. Every Atheron SDK carries the Kestrel API beside Sign in with Atheron, with the same package names, one error type, a pluggable transport and the usage headers read for you. Each client is held to the same kestrel conformance vectors. | Language | The Kestrel client | |---|---| | TypeScript | `AtheronKestrel` in @aton-network/sdk | | Python | `AtheronKestrel` in aton-network | | Go | `atheron.NewKestrel` | | Rust | `aton_network::Kestrel` | | Ruby | `AtonNetwork::Kestrel` | | PHP | `AtonNetwork\Kestrel` | | Elixir | `AtonNetwork.Kestrel` | | Java and Kotlin | `org.atonnetwork.atheron.Kestrel` | | C# | `AtonNetwork.Kestrel` | **cURL** ```bash # Which way ATON Kestrel leans on BTCUSDT now, pinned to a version. The answer's headers carry the month's usage. curl -s -D - "https://developers.aton-network.org/api/kestrel/v1/signals?symbol=BTCUSDT" \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Kestrel-Version: v0.1.0" ``` **TypeScript** ```ts // Which way ATON Kestrel leans on a market now, pinned to a version, and the month's calls so far. export async function kestrelLeaning(apiKey: string, symbol = "BTCUSDT"): Promise<{ leaning: string[]; callsUsed: number | null }> { const answer = await fetch(`https://developers.aton-network.org/api/kestrel/v1/signals?symbol=${symbol}`, { headers: { authorization: `Bearer ${apiKey}`, "kestrel-version": "v0.1.0" }, }); if (!answer.ok) throw new Error(`the Kestrel API answered ${answer.status}`); const { data } = (await answer.json()) as { data: { action: string; gate: { passed: boolean } }[] }; const used = answer.headers.get("kestrel-calls-used"); return { leaning: data.filter((s) => s.action !== "none" && s.gate.passed).map((s) => s.action), callsUsed: used === null ? null : Number(used) }; } ``` **Python** ```python # Which way ATON Kestrel leans on a market now, after fees, and what the month's calls stand at. from aton_network import AtheronKestrel def kestrel_view(api_key: str, symbol: str = "BTCUSDT") -> dict: kestrel = AtheronKestrel(api_key, version="v0.1.0") signals = kestrel.signals(symbol=symbol)["data"] leaning = [s for s in signals if s["action"] != "none" and s["gate"]["passed"]] return {"signals": len(signals), "leaning": [s["action"] for s in leaning], "calls_used": kestrel.usage["used"]} ``` **Rust** ```rust // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. use aton_network::{Error, Kestrel}; use serde_json::Value; pub async fn kestrel_leaning(api_key: &str, symbol: &str) -> Result<(Vec, Option), Error> { let kestrel = Kestrel::new(api_key)?.with_version("v0.1.0")?; let answer: Value = kestrel.signals(Some(symbol), None, None).await?; let leaning = answer["data"] .as_array() .map(|all| all.iter().filter(|s| s["action"] != "none" && s["gate"]["passed"] == true).filter_map(|s| s["action"].as_str().map(str::to_string)).collect()) .unwrap_or_default(); Ok((leaning, kestrel.usage().used)) } ``` **Go** ```go // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. package kestrelsignals import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Leaning(ctx context.Context, apiKey, symbol string) ([]string, *int64, error) { kestrel, err := atheron.NewKestrel(apiKey, atheron.WithKestrelVersion("v0.1.0")) if err != nil { return nil, nil, err } var answer struct { Data []struct { Action string `json:"action"` Gate struct { Passed bool `json:"passed"` } `json:"gate"` } `json:"data"` } if err := kestrel.Signals(ctx, atheron.SignalsParams{Symbol: symbol}, &answer); err != nil { return nil, nil, err } leaning := []string{} for _, s := range answer.Data { if s.Action != "none" && s.Gate.Passed { leaning = append(leaning, s.Action) } } return leaning, kestrel.Usage.Used, nil } ``` **Java** ```java // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. package examples.kestrelsignals.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.Kestrel; public class KestrelLeaning { @SuppressWarnings("unchecked") public static List kestrelLeaning(String apiKey, String symbol) { Kestrel kestrel = new Kestrel(apiKey, "v0.1.0"); Map answer = (Map) kestrel.signals(symbol, null, null); return ((List>) answer.get("data")).stream() .filter(s -> !"none".equals(s.get("action")) && Boolean.TRUE.equals(((Map) s.get("gate")).get("passed"))) .map(s -> (String) s.get("action")) .toList(); } } ``` **Kotlin** ```kotlin // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. package examples.kestrelsignals.kotlin import org.atonnetwork.atheron.Kestrel @Suppress("UNCHECKED_CAST") fun kestrelLeaning(apiKey: String, symbol: String): List { val kestrel = Kestrel(apiKey, "v0.1.0") val answer = kestrel.signals(symbol, null, null) as Map return (answer["data"] as List>) .filter { it["action"] != "none" && (it["gate"] as Map)["passed"] == true } .map { it["action"] as String } } ``` **C#** ```csharp // Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. using AtonNetwork; namespace Examples.KestrelSignals; public static class KestrelLeaning { public static async Task<(List Leaning, long? CallsUsed)> RunAsync(string apiKey, string symbol = "BTCUSDT") { var kestrel = new Kestrel(apiKey, version: "v0.1.0"); var answer = await kestrel.SignalsAsync(symbol: symbol); var leaning = answer?["data"]?.AsArray() .Where(s => (string?)s?["action"] != "none" && (bool?)s?["gate"]?["passed"] == true) .Select(s => (string)s!["action"]!) .ToList() ?? []; return (leaning, kestrel.Usage.Used); } } ``` **PHP** ```php signals(symbol: $symbol)['data']; $leaning = array_values(array_map(fn (array $s) => $s['action'], array_filter($signals, fn (array $s) => $s['action'] !== 'none' && $s['gate']['passed']))); return ['leaning' => $leaning, 'callsUsed' => $kestrel->usage['used']]; } ``` **Ruby** ```ruby # Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. require "aton_network" def kestrel_leaning(api_key, symbol = "BTCUSDT") kestrel = AtonNetwork::Kestrel.new(api_key, version: "v0.1.0") signals = kestrel.signals(symbol: symbol)["data"] { leaning: signals.select { |s| s["action"] != "none" && s["gate"]["passed"] }.map { |s| s["action"] }, calls_used: kestrel.usage["used"] } end ``` **Elixir** ```elixir # Which way ATON Kestrel leans on a market now, after fees, and the month's calls so far. defmodule MyApp.Kestrel do def leaning(api_key, symbol \\ "BTCUSDT") do kestrel = AtonNetwork.Kestrel.new(api_key, version: "v0.1.0") with {:ok, %{"data" => signals}, usage} <- AtonNetwork.Kestrel.signals(kestrel, symbol: symbol) do leaning = for s <- signals, s["action"] != "none" and s["gate"]["passed"], do: s["action"] {:ok, leaning, usage["used"]} end end end ``` See the [SDKs](/docs/sdks) page for each language's install line, and the [reference](/docs/kestrel/reference) for every endpoint. The MCP server answers Kestrel questions too: ask it for the Kestrel reference, prices or versions. --- # Kestrel API changelog URL: https://developers.aton-network.org/docs/kestrel/changelog What changed in the Kestrel API, newest first. ## Before the first version - Seats, metering with version weights, paper-mode prices, plan discounts, usage alerts and the waitlist. - Endpoints for signals and explanations, streams, sentiment, the Kestrel Eye, backtests, and paper trading. - Selling opens when ATON Kestrel v0.1.0 passes its gate and is published. --- # API reference URL: https://developers.aton-network.org/docs/reference Every endpoint of Sign in with Atheron, the developer API and the MCP server. Sign in with Atheron is standard OpenID Connect, under `https://accounts.aton-network.org`. The developer API and the MCP server are under `https://developers.aton-network.org`. Read the discovery document rather than hard-coding addresses. The whole surface is also an [OpenAPI document](/openapi.json). ## Sign in with Atheron (OpenID Connect) | Endpoint | Method | Path | What it does | |---|---|---|---| | [Discovery document](/docs/reference/discovery) | `GET` | `/.well-known/openid-configuration` | Where every other endpoint is, and what the provider supports: scopes, claims, grants, algorithms and client authentication methods. | | [Signing keys (JWKS)](/docs/reference/jwks) | `GET` | `/.well-known/jwks.json` | The public keys that sign ID tokens and access tokens, as a JSON Web Key Set. | | [Authorize](/docs/reference/authorize) | `GET` | `/oauth/authorize` | The page you send somebody to. They sign in, see what you ask for, and come back to your redirect address with a one-time code. | | [Token: swap a code](/docs/reference/token) | `POST` | `/oauth/token` | Swaps the code from the redirect for an ID token, an access token and, with offline_access, a refresh token. | | [Token: refresh](/docs/reference/refresh) | `POST` | `/oauth/token` | Swaps a refresh token for a new access token and a new refresh token. The old refresh token stops working. | | [Userinfo](/docs/reference/userinfo) | `GET, POST` | `/oauth/userinfo` | Who the person is as of now: the claims their consent allows, read fresh from their account. | | [Revoke](/docs/reference/revoke) | `POST` | `/oauth/revoke` | Ends a refresh token and every token in its line. | ## Developer API and MCP | Endpoint | Method | Path | What it does | |---|---|---|---| | [Developer API: your app](/docs/reference/api-app) | `GET` | `/api/v1/app` | The application and environment your API key belongs to: its client ID, redirect addresses, scopes, approved scopes and counts. | | [Developer API: usage](/docs/reference/api-usage) | `GET` | `/api/v1/usage` | Calls, errors and latency per day and bucket for your key's environment. | | [Developer API: events stream](/docs/reference/api-events) | `GET` | `/api/v1/events` | Your app's events as Server-Sent Events, the same events and the same bodies as its webhooks, in order, resumable for 30 days. | | [MCP server](/docs/reference/mcp) | `POST` | `/mcp` | The Model Context Protocol server your AI tools connect to: the docs, the reference, examples by language, error lookup, the changelog, status, and, with a key, your app's settings, usage and sandbox calls. | ## Client authentication at the token endpoint | Method | How | |---|---| | client_secret_basic | HTTP Basic with client ID and secret (preferred) | | client_secret_post | client_id and client_secret in the form | | private_key_jwt | client_assertion_type and a client_assertion signed with your certificate's key; see [Secrets and certificates](/docs/credentials) | | none | Public apps: client_id in the form, PKCE only | --- # Discovery document URL: https://developers.aton-network.org/docs/reference/discovery Where every other endpoint is, and what the provider supports: scopes, claims, grants, algorithms and client authentication methods. `GET https://accounts.aton-network.org/.well-known/openid-configuration` ## When to use it Read it when your app starts, and let your OpenID Connect library configure itself from it. Never hard-code the other addresses. ## Authentication None. It is public. ## Parameters None. ## Request **cURL** ```bash # Read the discovery document: every endpoint, scope and algorithm the provider supports. curl https://accounts.aton-network.org/.well-known/openid-configuration ``` **TypeScript** ```ts // Read the discovery document, and let everything else follow from it. import { ISSUER } from "@aton-network/sdk"; export async function discovery(): Promise<{ issuer: string; token_endpoint: string; jwks_uri: string }> { const answer = await fetch(`${ISSUER}/.well-known/openid-configuration`); if (!answer.ok) throw new Error(`discovery answered ${answer.status}`); const doc = (await answer.json()) as { issuer: string; token_endpoint: string; jwks_uri: string }; // The issuer must be exactly ours, byte for byte. if (doc.issuer !== ISSUER) throw new Error("unexpected issuer"); return doc; } ``` ## Response ```http HTTP 200 content-type: application/json { "issuer": "https://accounts.aton-network.org", "authorization_endpoint": "https://accounts.aton-network.org/oauth/authorize", "token_endpoint": "https://accounts.aton-network.org/oauth/token", "userinfo_endpoint": "https://accounts.aton-network.org/oauth/userinfo", "revocation_endpoint": "https://accounts.aton-network.org/oauth/revoke", "jwks_uri": "https://accounts.aton-network.org/.well-known/jwks.json", "scopes_supported": ["openid", "profile", "email", "offline_access", "age", "membership", "wallet", "licences", "creator"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": ["ES256"], "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none", "private_key_jwt"], "token_endpoint_auth_signing_alg_values_supported": ["ES256", "RS256"], "code_challenge_methods_supported": ["S256"], "claims_supported": ["sub", "iss", "aud", "exp", "iat", "nonce", "sid", "email", "email_verified", "name", "creator", "plan", "age", "membership", "wallets"], "code_challenge_required": true } ``` ## Errors This endpoint returns no errors of its own; a fault at our end is a 5xx. ## Good to know - The issuer is exactly https://accounts.aton-network.org, with no trailing slash. Compare it byte for byte with the iss of every ID token. - Cache it for up to a day. It changes only when we add a scope, a claim or a method. ## Security - Fetch it over https only, and never from an address a user gave you. ## Try it [Read it in the sandbox](/sandbox). --- # Signing keys (JWKS) URL: https://developers.aton-network.org/docs/reference/jwks The public keys that sign ID tokens and access tokens, as a JSON Web Key Set. `GET https://accounts.aton-network.org/.well-known/jwks.json` ## When to use it Your JOSE library reads it to check token signatures. Fetch it again when a token names a kid you do not have. ## Authentication None. It is public. ## Parameters None. ## Request **cURL** ```bash # The public keys that sign ID tokens and access tokens (ES256), matched on kid. curl https://accounts.aton-network.org/.well-known/jwks.json ``` **TypeScript** ```ts // The signing keys, as a JSON Web Key Set. A JOSE library (for example createRemoteJWKSet in jose) // fetches and caches these for you; this shows what it reads. import { ISSUER } from "@aton-network/sdk"; export async function signingKeys(): Promise<{ kid: string; alg: string }[]> { const answer = await fetch(`${ISSUER}/.well-known/jwks.json`); if (!answer.ok) throw new Error(`jwks answered ${answer.status}`); const { keys } = (await answer.json()) as { keys: { kid: string; alg: string }[] }; // We sign with ES256 only: refuse anything else. return keys.filter((k) => k.alg === "ES256"); } ``` ## Response ```http HTTP 200 content-type: application/json cache-control: public, max-age=300, stale-while-revalidate=86400 { "keys": [ { "kty": "EC", "crv": "P-256", "alg": "ES256", "use": "sig", "kid": "2026-10-01", "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0" } ] } ``` ## Errors This endpoint returns no errors of its own; a fault at our end is a 5xx. ## Good to know - Tokens are signed with ES256 only. Refuse any other alg, and never accept alg none. - Keys rotate with an overlap: a new key appears in the set before anything is signed with it. ## Security - Match on kid, and refuse a token whose kid is not in the set after one refresh of it. --- # Authorize URL: https://developers.aton-network.org/docs/reference/authorize The page you send somebody to. They sign in, see what you ask for, and come back to your redirect address with a one-time code. `GET https://accounts.aton-network.org/oauth/authorize` ## When to use it Every time somebody chooses Sign in with Atheron in your app. Build the address on your server, keep the verifier, state and nonce for this browser, then redirect. ## Authentication None: it identifies your app by client_id and proves the request is yours with PKCE. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `response_type` | query | string | yes | code | Always code. The implicit and hybrid flows are not offered. | | `client_id` | query | string | yes | test-<16 hex> or live-<16 hex> | Your test or live client ID, from the app's Sign-in page. | | `redirect_uri` | query | string | yes | one registered address, matched exactly, up to 500 characters | Where we send the person back. No wildcards and no prefixes: the whole address must equal one you registered. | | `scope` | query | string | yes | space separated; openid is added if missing | What you ask for. Every scope is shown on the consent screen. See Scopes and claims. | | `code_challenge` | query | string | yes | 43 to 128 characters, base64url | BASE64URL(SHA-256(code_verifier)). Required of every client, including server apps that hold a secret. | | `code_challenge_method` | query | string | yes | S256 | plain is refused. | | `state` | query | string | no | up to 500 characters | Returned unchanged. Make it random per sign-in, keep it, and compare it in constant time when the person comes back. | | `nonce` | query | string | no | up to 500 characters | Copied into the ID token. Check it there to stop a token being replayed into another sign-in. | | `prompt` | query | string | no | none, login, consent or create | none: answer at once or send an error. login: ask them to sign in again. consent: show the consent screen even if they agreed before. create: open registration. | | `max_age` | query | integer | no | whole seconds, 0 or more | The oldest sign-in you accept. Older, and they sign in again. | | `login_hint` | query | string | no | an email address | With prompt=create, starts registration with this address filled in. | ## Request **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## Response ```http HTTP 200 content-type: application/json cache-control: no-store { "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...", "token_type": "Bearer", "expires_in": 900, "scope": "openid profile email offline_access", "id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjYtMTAtMDEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmF0b24tbmV0d29yay5vcmciLCJzdWIiOiI5YTZk...", "refresh_token": "ath_rt_6Qm9..." } ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 400 | [`invalid_request`](/docs/errors#invalid-request) | A parameter is missing or malformed: no PKCE, PKCE with plain, a bad max_age, prompt=none combined with another prompt, or a token request that is not form encoded. | Send every required parameter once, PKCE with S256, and form-encode the token request. | | 401 | [`invalid_client`](/docs/errors#invalid-client) | Your app could not be authenticated: a wrong or expired secret, a revoked or expired certificate, a bad client_assertion (aud must be the token endpoint, exp at most ten minutes ahead, jti new), a public app sending a secret, or the app is suspended. | Check the credential on the app's Credentials page and its expiry. Rotate without downtime: add a new one, deploy, then revoke the old. | | 400 | [`invalid_grant`](/docs/errors#invalid-grant) | The code or refresh token cannot be used: the code expired (60 seconds) or was used, it was issued to another client, the redirect_uri or code_verifier does not match, a refresh token was used twice, access was withdrawn, or the tester was removed. | Start the sign-in again. Swap each code once, straight away, with the same redirect_uri and the verifier you kept. | | 400 | [`unsupported_grant_type`](/docs/errors#unsupported-grant-type) | grant_type is not authorization_code or refresh_token. | Use one of the two. | | 429 | [`rate_limited`](/docs/errors#rate-limited) | Over your quota for this bucket, this minute or this day. | Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. | ## Good to know - The body must be application/x-www-form-urlencoded. JSON is refused with invalid_request. - Check the ID token before you trust it: signature (ES256, from the keys), iss, aud is your client ID, exp, and nonce. - A refresh token comes back only when you asked for offline_access and the person agreed. ## Security - Never call this from a browser with a secret. A public app sends client_id and its PKCE verifier only. - Send an assertion or a secret, never both. ## Try it [Swap a code in the sandbox](/sandbox). --- # Token: refresh URL: https://developers.aton-network.org/docs/reference/refresh Swaps a refresh token for a new access token and a new refresh token. The old refresh token stops working. `POST https://accounts.aton-network.org/oauth/token` ## When to use it When an access token has expired (15 minutes) and you need userinfo again while the person is away. ## Authentication The same client authentication as the code swap. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `grant_type` | form | string | yes | refresh_token | This grant. | | `refresh_token` | form | string | yes | | The newest refresh token you hold. | | `scope` | form | string | no | | Fewer scopes than were granted, to narrow the new access token. Never more. | | `client_id` | form | string | no | | Required for a public app. | ## Request **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Response ```http HTTP 200 content-type: application/json cache-control: no-store { "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...", "token_type": "Bearer", "expires_in": 900, "scope": "openid profile email offline_access", "refresh_token": "ath_rt_k2Lp..." } ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 400 | [`invalid_request`](/docs/errors#invalid-request) | A parameter is missing or malformed: no PKCE, PKCE with plain, a bad max_age, prompt=none combined with another prompt, or a token request that is not form encoded. | Send every required parameter once, PKCE with S256, and form-encode the token request. | | 401 | [`invalid_client`](/docs/errors#invalid-client) | Your app could not be authenticated: a wrong or expired secret, a revoked or expired certificate, a bad client_assertion (aud must be the token endpoint, exp at most ten minutes ahead, jti new), a public app sending a secret, or the app is suspended. | Check the credential on the app's Credentials page and its expiry. Rotate without downtime: add a new one, deploy, then revoke the old. | | 400 | [`invalid_grant`](/docs/errors#invalid-grant) | The code or refresh token cannot be used: the code expired (60 seconds) or was used, it was issued to another client, the redirect_uri or code_verifier does not match, a refresh token was used twice, access was withdrawn, or the tester was removed. | Start the sign-in again. Swap each code once, straight away, with the same redirect_uri and the verifier you kept. | | 429 | [`rate_limited`](/docs/errors#rate-limited) | Over your quota for this bucket, this minute or this day. | Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. | ## Good to know - Refresh tokens rotate. Keep only the newest, and send one refresh at a time for a person. - A refresh token used twice ends its whole line, because one of the two uses was not yours. - A refresh token expires after 90 days without use, and every token of a line expires one year after the sign-in it began with, however often it is used. ## Security - Store refresh tokens encrypted at rest, and never send them to a browser. --- # Userinfo URL: https://developers.aton-network.org/docs/reference/userinfo Who the person is as of now: the claims their consent allows, read fresh from their account. `GET or POST https://accounts.aton-network.org/oauth/userinfo` ## When to use it When you need current details (a plan, a verified age, linked wallets) rather than what the ID token said when they signed in. ## Authentication Authorization: Bearer . ID tokens are refused. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `Authorization` | header | string | yes | Bearer | An access token issued to your client, under 15 minutes old. | ## Request **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Response ```http HTTP 200 content-type: application/json cache-control: no-store { "sub": "9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11", "name": "Ada Lovelace", "email": "ada@example.com", "email_verified": true, "age": { "verified": true, "adult": true, "sixteen_or_over": true } } ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 401 | [`invalid_token`](/docs/errors#invalid-token) | Not an access token (ID tokens are refused), expired (15 minutes), the account is suspended, access was withdrawn, or the app is suspended. | Refresh the access token, or sign the person in again. Never send an ID token to userinfo. | | 429 | [`rate_limited`](/docs/errors#rate-limited) | Over your quota for this bucket, this minute or this day. | Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. | ## Good to know - Only the claims of scopes the person agreed to come back. A claim you did not ask for is never there. - A call that reads a sensitive scope (age, membership, wallet, licences, creator) also counts against the smaller userinfo:sensitive bucket. ## Security - Key your records on sub, never on email: people change their email. ## Try it [Call it with a test token in the sandbox](/sandbox). --- # Revoke URL: https://developers.aton-network.org/docs/reference/revoke Ends a refresh token and every token in its line. `POST https://accounts.aton-network.org/oauth/revoke` ## When to use it When somebody signs out of your app for good, or disconnects their Atheron account from it. ## Authentication The same client authentication as the token endpoint. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `token` | form | string | yes | | The refresh token to end. | | `token_type_hint` | form | string | no | refresh_token | Optional. | ## Request **cURL** ```bash # End a refresh token and every token in its line. Always answers 200 once your app is authenticated. curl https://accounts.aton-network.org/oauth/revoke \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d token="$REFRESH_TOKEN" \ -d token_type_hint=refresh_token ``` **TypeScript** ```ts // When somebody signs out of your app for good, end their refresh token. import { ISSUER } from "@aton-network/sdk"; export async function revokeRefreshToken(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/revoke`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token: refreshToken, token_type_hint: "refresh_token" }), }); if (!answer.ok) throw new Error(`revoke answered ${answer.status}`); } ``` **Python** ```python # When somebody disconnects your app: revoke their refresh token, then delete it. import os from aton_network import Client def revoke_refresh_token(refresh_token: str) -> None: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) client.revoke_token(refresh_token, "refresh_token") ``` **Rust** ```rust // Revoke a refresh token when somebody signs out or disconnects your app. use aton_network::{Client, Error}; pub async fn revoke_refresh_token(refresh_token: &str) -> Result<(), Error> { Client::from_env().revoke_token(refresh_token, Some("refresh_token")).await } ``` **Go** ```go // Revoke a refresh token when somebody signs out or disconnects your app. package revoke import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RevokeRefreshToken(ctx context.Context, refreshToken string) error { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RevokeToken(ctx, refreshToken, "refresh_token") } ``` **Java** ```java // Revoke a refresh token when somebody signs out or disconnects your app. package examples.revoke.java; import org.atonnetwork.atheron.AtheronClient; public class RevokeRefreshToken { public static void revokeRefreshToken(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); client.revokeToken(refreshToken, "refresh_token"); } } ``` **Kotlin** ```kotlin // Revoke a refresh token when somebody signs out or disconnects your app. package examples.revoke.kotlin import org.atonnetwork.atheron.AtheronClient fun revokeRefreshToken(refreshToken: String) { AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).revokeToken(refreshToken, "refresh_token") } ``` **C#** ```csharp // Revoke a refresh token when somebody signs out or disconnects your app. using AtonNetwork; namespace Examples.Revoke; public static class RevokeRefreshToken { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RevokeTokenAsync(refreshToken, "refresh_token"); } } ``` **PHP** ```php revokeToken($refreshToken, 'refresh_token'); } ``` **Ruby** ```ruby # When somebody signs out of your app for good, end their refresh token. require "aton_network" def revoke_refresh_token(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.revoke_token(refresh_token, "refresh_token") end ``` **Elixir** ```elixir # When somebody signs out of your app for good, end their refresh token. defmodule MyApp.Revoke do def revoke_refresh_token(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.revoke_token(refresh_token, "refresh_token") end end ``` ## Response ```http HTTP 200 content-type: application/json {} ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 400 | [`invalid_request`](/docs/errors#invalid-request) | A parameter is missing or malformed: no PKCE, PKCE with plain, a bad max_age, prompt=none combined with another prompt, or a token request that is not form encoded. | Send every required parameter once, PKCE with S256, and form-encode the token request. | | 401 | [`invalid_client`](/docs/errors#invalid-client) | Your app could not be authenticated: a wrong or expired secret, a revoked or expired certificate, a bad client_assertion (aud must be the token endpoint, exp at most ten minutes ahead, jti new), a public app sending a secret, or the app is suspended. | Check the credential on the app's Credentials page and its expiry. Rotate without downtime: add a new one, deploy, then revoke the old. | ## Good to know - It always answers 200 once your app is authenticated, whether or not the token was real, so the answer says nothing about tokens to a guesser (RFC 7009). ## Security - Revoke on sign-out from a shared device. The person's consent stays until they take it back in their account. --- # Developer API: your app URL: https://developers.aton-network.org/docs/reference/api-app The application and environment your API key belongs to: its client ID, redirect addresses, scopes, approved scopes and counts. `GET https://developers.aton-network.org/api/v1/app` ## When to use it In deploy checks: confirm the redirect addresses and scopes your code expects are the ones registered. ## Authentication Authorization: Bearer with the app:read right. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `Authorization` | header | string | yes | Bearer ath_test_... or ath_live_... | An API key from the app's API keys page. | ## Request **REST, cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **REST, TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **REST, Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **REST, Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **REST, Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **REST, Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **REST, Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **REST, C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **REST, PHP** ```php app()['redirect_uris']; } ``` **REST, Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **REST, Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` **GraphQL, cURL** ```bash # Your app as registered, through GraphQL: ask for the fields you need. Needs app:read; costs 1. curl https://developers.aton-network.org/api/graphql \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"query App {\n app {\n name\n state\n env\n clientId\n redirectUris\n scopes\n }\n}\n"}' ``` **GraphQL, TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. const APP = `query App { app { name state env clientId redirectUris scopes } } `; export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/graphql", { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, body: JSON.stringify({ query: APP }), }); const { data, errors } = (await answer.json()) as { data?: { app: { redirectUris: string[] } | null }; errors?: { message: string; extensions: { code: string } }[] }; if (errors?.length) throw new Error(`${errors[0]!.extensions.code}: ${errors[0]!.message}`); return data!.app!.redirectUris; } ``` **GraphQL, Python** ```python # In a deploy check: confirm the redirect addresses your code expects are the ones registered. import os import requests APP = """query App { app { name state env clientId redirectUris scopes } } """ def registered_redirects(api_key: str) -> list[str]: answer = requests.post( "https://developers.aton-network.org/api/graphql", headers={"Authorization": f"Bearer {api_key}"}, json={"query": APP}, timeout=10, ) body = answer.json() if body.get("errors"): error = body["errors"][0] raise RuntimeError(f"{error['extensions']['code']}: {error['message']}") return body["data"]["app"]["redirectUris"] if __name__ == "__main__": print(registered_redirects(os.environ["ATHERON_API_KEY"])) ``` ## In GraphQL `Query.app` at `POST https://developers.aton-network.org/api/graphql`, with the `app:read` right, costing 1. Your application as registered, for the environment the key belongs to. The same fields as the REST call, in camelCase; ask only for the ones you need. ```graphql query App { app { name state env clientId redirectUris scopes } } ``` The answer: ```json { "data": { "app": { "name": "Acme Notes", "state": "development", "env": "TEST", "clientId": "test-3f9c2a", "redirectUris": [ "https://app.example.com/callback" ], "scopes": [ "openid", "profile", "email" ] } }, "extensions": { "cost": 1 } } ``` Errors it can meet: [`invalid_key`](/docs/reference/graphql#invalid-key), [`missing_right`](/docs/reference/graphql#missing-right), [`not_found`](/docs/reference/graphql#not-found), [`rate_limited`](/docs/reference/graphql#rate-limited), [`server_error`](/docs/reference/graphql#server-error). Try it in the [GraphQL playground](/graphql). ## Response ```http HTTP 200 content-type: application/json { "name": "Price Check", "state": "live", "kind": "confidential", "quota": "free", "env": "live", "client_id": "live-1a2b3c4d5e6f7a8b", "redirect_uris": ["https://app.example.com/callback"], "scopes": ["openid", "profile", "email"], "approved_scopes": ["openid", "profile", "email"], "privacy_url": "https://app.example.com/privacy", "homepage_url": "https://app.example.com", "credentials": 2, "testers": 0, "webhooks": 1 } ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 401 | [`invalid_key`](/docs/errors#invalid-key) | No API key was sent, or it is not one of ours, expired or revoked. | Send Authorization: Bearer with a key from the app's API keys page. | | 403 | [`missing_right`](/docs/errors#missing-right) | The key does not carry the right this call needs. | Make a key with the right (app:read or usage:read). | | 429 | [`rate_limited`](/docs/errors#rate-limited) | Over your quota for this bucket, this minute or this day. | Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. | | 404 | [`not_found`](/docs/errors#not-found) | The application this key belonged to has been removed. | Use a key of an app that still exists. | | 500 | [`server_error`](/docs/errors#server-error) | Something went wrong at our end. | Try again in a moment. If it keeps happening, check the status page and tell us in the chat. | ## Good to know - Counts the api bucket of your quota. ## Security - Never put an API key in a URL or in code that ships to a browser. --- # Developer API: usage URL: https://developers.aton-network.org/docs/reference/api-usage Calls, errors and latency per day and bucket for your key's environment. `GET https://developers.aton-network.org/api/v1/usage` ## When to use it In your own dashboards and alerts, for example to warn before you reach a daily limit. ## Authentication Authorization: Bearer with the usage:read right. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `Authorization` | header | string | yes | | An API key with usage:read. | | `days` | query | integer | no | 1 to 90, default 7 | How many days back, today included. | ## Request **REST, cURL** ```bash # Calls, errors and latency per day and bucket, for the last seven days. Needs usage:read. curl "https://developers.aton-network.org/api/v1/usage?days=7" \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **REST, TypeScript** ```ts // Today's token calls, for your own alert before you reach a daily limit. export async function tokenCallsToday(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/usage?days=1", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const { usage } = (await answer.json()) as { usage: { bucket: string; calls: number }[] }; return usage.filter((row) => row.bucket === "token").reduce((sum, row) => sum + row.calls, 0); } ``` **REST, Python** ```python # How many token calls the app made today, for your own alerts. from aton_network import AtheronApi def token_calls_today(api_key: str) -> int: usage = AtheronApi(api_key).usage(days=1)["usage"] return sum(row["calls"] for row in usage if row["bucket"] == "token") ``` **REST, Rust** ```rust // How many token calls the app made today, for your own alerts. use aton_network::{Api, Error}; pub async fn token_calls_today(api_key: &str) -> Result { let usage = Api::new(api_key)?.usage(Some(1)).await?; Ok(usage.usage.iter().filter(|row| row.bucket == "token").map(|row| row.calls).sum()) } ``` **REST, Go** ```go // How many token calls the app made today, for your own alerts. package apiusage import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func TokenCallsToday(ctx context.Context, apiKey string) (int64, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return 0, err } usage, err := api.Usage(ctx, &atheron.UsageParams{Days: 1}) if err != nil { return 0, err } var calls int64 for _, row := range usage.Usage { if row.Bucket == "token" { calls += row.Calls } } return calls, nil } ``` **REST, Java** ```java // How many token calls the app made today, for your own alerts. package examples.apiusage.java; import org.atonnetwork.atheron.AtheronApi; public class TokenCallsToday { public static long tokenCallsToday(String apiKey) { return new AtheronApi(apiKey).usage(1).usage().stream() .filter(row -> "token".equals(row.bucket())) .mapToLong(row -> row.calls()) .sum(); } } ``` **REST, Kotlin** ```kotlin // How many token calls the app made today, for your own alerts. package examples.apiusage.kotlin import org.atonnetwork.atheron.AtheronApi fun tokenCallsToday(apiKey: String): Long = AtheronApi(apiKey).usage(1).usage().filter { it.bucket() == "token" }.sumOf { it.calls() } ``` **REST, C#** ```csharp // How many token calls the app made today, for your own alerts. using AtonNetwork; namespace Examples.ApiUsage; public static class TokenCallsToday { public static async Task RunAsync(string apiKey) { var usage = await new AtheronApi(apiKey).UsageAsync(days: 1); return usage.Usage?.Where(row => row.Bucket == "token").Sum(row => row.Calls ?? 0) ?? 0; } } ``` **REST, PHP** ```php usage(days: 1)['usage']; return array_sum(array_column(array_filter($rows, fn (array $row) => $row['bucket'] === 'token'), 'calls')); } ``` **REST, Ruby** ```ruby # Today's token calls, for your own alert before you reach a daily limit. require "aton_network" def token_calls_today(api_key) AtonNetwork::Api.new(api_key).usage(days: 1)["usage"].select { |row| row["bucket"] == "token" }.sum { |row| row["calls"] } end ``` **REST, Elixir** ```elixir # Today's token calls, for your own alert before you reach a daily limit. defmodule MyApp.Usage do def token_calls_today(api_key) do with {:ok, %{"usage" => rows}} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.usage(days: 1) do {:ok, rows |> Enum.filter(&(&1["bucket"] == "token")) |> Enum.map(& &1["calls"]) |> Enum.sum()} end end end ``` **GraphQL, cURL** ```bash # Calls, errors and latency per day and bucket for the last seven days. Needs usage:read; costs 1. curl https://developers.aton-network.org/api/graphql \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"query Usage($days: Int = 7) {\n usage(days: $days) {\n env\n days\n usage {\n day\n bucket\n calls\n errors\n msAvg\n msMax\n }\n }\n}\n","variables":{"days":7}}' ``` **GraphQL, TypeScript** ```ts // Today's token calls, for your own alert before you reach a daily limit. const USAGE = `query Usage($days: Int = 7) { usage(days: $days) { env days usage { day bucket calls errors msAvg msMax } } } `; export async function tokenCallsToday(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/graphql", { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, body: JSON.stringify({ query: USAGE, variables: { days: 1 } }), }); const { data, errors } = (await answer.json()) as { data?: { usage: { usage: { bucket: string; calls: number }[] } | null }; errors?: { message: string; extensions: { code: string } }[] }; if (errors?.length) throw new Error(`${errors[0]!.extensions.code}: ${errors[0]!.message}`); return data!.usage!.usage.filter((row) => row.bucket === "token").reduce((sum, row) => sum + row.calls, 0); } ``` **GraphQL, Python** ```python # Today's token calls, for your own alert before you reach a daily limit. import os import requests USAGE = """query Usage($days: Int = 7) { usage(days: $days) { env days usage { day bucket calls errors msAvg msMax } } } """ def token_calls_today(api_key: str) -> int: answer = requests.post( "https://developers.aton-network.org/api/graphql", headers={"Authorization": f"Bearer {api_key}"}, json={"query": USAGE, "variables": {"days": 1}}, timeout=10, ) body = answer.json() if body.get("errors"): error = body["errors"][0] raise RuntimeError(f"{error['extensions']['code']}: {error['message']}") return sum(row["calls"] for row in body["data"]["usage"]["usage"] if row["bucket"] == "token") if __name__ == "__main__": print(token_calls_today(os.environ["ATHERON_API_KEY"])) ``` ## In GraphQL `Query.usage` at `POST https://developers.aton-network.org/api/graphql`, with the `usage:read` right, costing 1. Calls, errors and latency per day and bucket for the key's environment, 1 to 90 days (anything else is brought within, as REST does). ```graphql query Usage($days: Int = 7) { usage(days: $days) { env days usage { day bucket calls errors msAvg msMax } } } ``` Variables: ```json { "days": 7 } ``` The answer: ```json { "data": { "usage": { "env": "TEST", "days": 7, "usage": [ { "day": "2026-10-14", "bucket": "token", "calls": 42, "errors": 1, "msAvg": 38, "msMax": 210 } ] } }, "extensions": { "cost": 1 } } ``` Errors it can meet: [`invalid_key`](/docs/reference/graphql#invalid-key), [`missing_right`](/docs/reference/graphql#missing-right), [`rate_limited`](/docs/reference/graphql#rate-limited), [`server_error`](/docs/reference/graphql#server-error). Try it in the [GraphQL playground](/graphql). ## Response ```http HTTP 200 content-type: application/json { "env": "live", "days": 7, "usage": [ { "day": "2026-10-14", "bucket": "token", "calls": 1203, "errors": 4, "ms_avg": 38, "ms_max": 412 }, { "day": "2026-10-14", "bucket": "userinfo", "calls": 2210, "errors": 0, "ms_avg": 22, "ms_max": 190 } ] } ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 401 | [`invalid_key`](/docs/errors#invalid-key) | No API key was sent, or it is not one of ours, expired or revoked. | Send Authorization: Bearer with a key from the app's API keys page. | | 403 | [`missing_right`](/docs/errors#missing-right) | The key does not carry the right this call needs. | Make a key with the right (app:read or usage:read). | | 429 | [`rate_limited`](/docs/errors#rate-limited) | Over your quota for this bucket, this minute or this day. | Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. | | 500 | [`server_error`](/docs/errors#server-error) | Something went wrong at our end. | Try again in a moment. If it keeps happening, check the status page and tell us in the chat. | ## Good to know - A day with no calls in a bucket is left out rather than listed as zero. ## Security - Never put an API key in a URL or in code that ships to a browser. --- # Developer API: events stream URL: https://developers.aton-network.org/docs/reference/api-events Your app's events as Server-Sent Events, the same events and the same bodies as its webhooks, in order, resumable for 30 days. `GET https://developers.aton-network.org/api/v1/events` ## When to use it When you would rather hold a connection open than run a public address for webhooks: a worker, a desktop app's server, a test. The WebSocket form is wss://developers.aton-network.org/api/v1/events/ws. ## Authentication Authorization: Bearer with the events:read right. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `Authorization` | header | string | yes | | An API key with events:read. | | `Last-Event-ID` | header | string | no | | The id of the last event you handled; the stream sends what you missed first. EventSource-style clients send it on their own. | | `after` | query | string | no | | The same as Last-Event-ID, for clients that cannot set headers. | | `types` | query | string | no | user.signed_in, consent.revoked, membership.changed, ping, comma separated | Only these event types. | ## Request **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` ## Response ```http HTTP 200 content-type: text/event-stream retry: 3000 event: ready data: {"types":null} id: 5b1c2a8e-0000-4000-8000-000000000001 event: user.signed_in data: {"id":"5b1c2a8e-0000-4000-8000-000000000001","type":"user.signed_in","created":"2026-10-15T17:02:11.000Z","client_id":"live-1a2b3c4d5e6f7a8b","data":{"sub":"9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11","scopes":["openid","email"],"at":"2026-10-15T17:02:11.000Z"}} : beat ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 401 | [`invalid_key`](/docs/errors#invalid-key) | No API key was sent, or it is not one of ours, expired or revoked. | Send Authorization: Bearer with a key from the app's API keys page. | | 403 | [`missing_right`](/docs/errors#missing-right) | The key does not carry the right this call needs. | Make a key with the right (app:read or usage:read). | | 429 | [`rate_limited`](/docs/errors#rate-limited) | Over your quota for this bucket, this minute or this day. | Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. | | 400 | [`invalid_request`](/docs/errors#invalid-request) | A parameter is missing or malformed: no PKCE, PKCE with plain, a bad max_age, prompt=none combined with another prompt, or a token request that is not form encoded. | Send every required parameter once, PKCE with S256, and form-encode the token request. | | 500 | [`server_error`](/docs/errors#server-error) | Something went wrong at our end. | Try again in a moment. If it keeps happening, check the status page and tell us in the chat. | ## Good to know - Opening a stream counts as one call against the api quota; the events themselves do not. - A comment line (: beat) comes every 15 seconds so proxies keep the connection open. An error after the stream started arrives as event: error, then the stream closes. - The SDKs follow the stream for you, reconnecting and resuming from the last id: followEvents in @aton-network/sdk, follow_events in atheron for Python. ## Security - Never put an API key in a URL or in code that ships to a browser: open the stream from your server. --- # MCP server URL: https://developers.aton-network.org/docs/reference/mcp The Model Context Protocol server your AI tools connect to: the docs, the reference, examples by language, error lookup, the changelog, status, and, with a key, your app's settings, usage and sandbox calls. `POST https://developers.aton-network.org/mcp` ## When to use it Connect Claude, your editor or any MCP client to it while you build. ## Authentication None for the docs tools. A test or live API key as Authorization: Bearer for your app's data; a test key with the sandbox right for sandbox calls. ## Parameters | Name | In | Type | Required | Limits | What it is | |---|---|---|---|---|---| | `body` | body | object | yes | one JSON-RPC 2.0 message, up to 64 KB | initialize, tools/list or tools/call (MCP 2025-06-18, Streamable HTTP, stateless). | | `Authorization` | header | string | no | | An API key, for the tools that read your app. | ## Request **cURL** ```bash # Start an MCP session by hand: one JSON-RPC message in, one answer out. curl https://developers.aton-network.org/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' ``` **TypeScript** ```ts // List the MCP server's tools from your own code. Most people connect an MCP client instead. export async function listTools(): Promise { const answer = await fetch("https://developers.aton-network.org/mcp", { method: "POST", headers: { "content-type": "application/json", accept: "application/json, text/event-stream" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }), }); const { result } = (await answer.json()) as { result: { tools: { name: string }[] } }; return result.tools.map((tool) => tool.name); } ``` ## Response ```http HTTP 200 content-type: application/json { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { "name": "atheron-developers", "version": "0.2.0" } } } ``` ## Errors | HTTP | error | Cause | Fix | |---|---|---|---| | 401 | [`invalid_key`](/docs/errors#invalid-key) | No API key was sent, or it is not one of ours, expired or revoked. | Send Authorization: Bearer with a key from the app's API keys page. | | 429 | [`rate_limited`](/docs/errors#rate-limited) | Over your quota for this bucket, this minute or this day. | Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. | ## Good to know - Every tool is read-only on production. A notification is answered 202 with no body. ## Security - Use a test key while you build. A live key never carries the sandbox right. --- # GraphQL URL: https://developers.aton-network.org/docs/reference/graphql The same API as GraphQL: operations, the schema, limits, errors and persisted queries. `POST https://developers.aton-network.org/api/graphql` (or GET for a persisted query), with an API key as `Authorization: Bearer`. The same functions answer it as the REST API, so the data, the rights and the quota are the same: a request takes its cost from the api bucket. Subscriptions go over `wss://developers.aton-network.org/api/graphql/ws` (graphql-transport-ws). - The schema: [https://developers.aton-network.org/graphql/schema.graphql](https://developers.aton-network.org/graphql/schema.graphql), versioned; a field is removed only after six months deprecated. - The [playground](/graphql) runs your queries with your key. - Every code block on the REST reference pages has a REST and a GraphQL side: pick one and the site remembers it. ## Operations | Field | REST twin | Right | Cost | What it does | |---|---|---|---|---| | `Query.app` | `GET /api/v1/app` | app:read | 1 | Your application as registered, for the environment the key belongs to. The same fields as the REST call, in camelCase; ask only for the ones you need. | | `Query.usage` | `GET /api/v1/usage` | usage:read | 1 | Calls, errors and latency per day and bucket for the key's environment, 1 to 90 days (anything else is brought within, as REST does). | | `Query.app, Query.usage` | none | app:read and usage:read | 2 | Two REST calls in one request. It costs two, as the two calls would, and a key with only one of the rights gets that half and a missing_right error for the other. | | `Subscription.events` | `GET /api/v1/events` | events:read | 1 | The events your webhooks hear, over a WebSocket (graphql-transport-ws). Filter by type; resume after the last id you were sent, within 30 days. Costs one when it starts; events are not counted. | ### Read your app **REST, cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **REST, TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **REST, Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **REST, Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **REST, Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **REST, Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **REST, Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **REST, C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **REST, PHP** ```php app()['redirect_uris']; } ``` **REST, Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **REST, Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` **GraphQL, cURL** ```bash # Your app as registered, through GraphQL: ask for the fields you need. Needs app:read; costs 1. curl https://developers.aton-network.org/api/graphql \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"query App {\n app {\n name\n state\n env\n clientId\n redirectUris\n scopes\n }\n}\n"}' ``` **GraphQL, TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. const APP = `query App { app { name state env clientId redirectUris scopes } } `; export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/graphql", { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, body: JSON.stringify({ query: APP }), }); const { data, errors } = (await answer.json()) as { data?: { app: { redirectUris: string[] } | null }; errors?: { message: string; extensions: { code: string } }[] }; if (errors?.length) throw new Error(`${errors[0]!.extensions.code}: ${errors[0]!.message}`); return data!.app!.redirectUris; } ``` **GraphQL, Python** ```python # In a deploy check: confirm the redirect addresses your code expects are the ones registered. import os import requests APP = """query App { app { name state env clientId redirectUris scopes } } """ def registered_redirects(api_key: str) -> list[str]: answer = requests.post( "https://developers.aton-network.org/api/graphql", headers={"Authorization": f"Bearer {api_key}"}, json={"query": APP}, timeout=10, ) body = answer.json() if body.get("errors"): error = body["errors"][0] raise RuntimeError(f"{error['extensions']['code']}: {error['message']}") return body["data"]["app"]["redirectUris"] if __name__ == "__main__": print(registered_redirects(os.environ["ATHERON_API_KEY"])) ``` ### Read your usage **REST, cURL** ```bash # Calls, errors and latency per day and bucket, for the last seven days. Needs usage:read. curl "https://developers.aton-network.org/api/v1/usage?days=7" \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **REST, TypeScript** ```ts // Today's token calls, for your own alert before you reach a daily limit. export async function tokenCallsToday(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/usage?days=1", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const { usage } = (await answer.json()) as { usage: { bucket: string; calls: number }[] }; return usage.filter((row) => row.bucket === "token").reduce((sum, row) => sum + row.calls, 0); } ``` **REST, Python** ```python # How many token calls the app made today, for your own alerts. from aton_network import AtheronApi def token_calls_today(api_key: str) -> int: usage = AtheronApi(api_key).usage(days=1)["usage"] return sum(row["calls"] for row in usage if row["bucket"] == "token") ``` **REST, Rust** ```rust // How many token calls the app made today, for your own alerts. use aton_network::{Api, Error}; pub async fn token_calls_today(api_key: &str) -> Result { let usage = Api::new(api_key)?.usage(Some(1)).await?; Ok(usage.usage.iter().filter(|row| row.bucket == "token").map(|row| row.calls).sum()) } ``` **REST, Go** ```go // How many token calls the app made today, for your own alerts. package apiusage import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func TokenCallsToday(ctx context.Context, apiKey string) (int64, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return 0, err } usage, err := api.Usage(ctx, &atheron.UsageParams{Days: 1}) if err != nil { return 0, err } var calls int64 for _, row := range usage.Usage { if row.Bucket == "token" { calls += row.Calls } } return calls, nil } ``` **REST, Java** ```java // How many token calls the app made today, for your own alerts. package examples.apiusage.java; import org.atonnetwork.atheron.AtheronApi; public class TokenCallsToday { public static long tokenCallsToday(String apiKey) { return new AtheronApi(apiKey).usage(1).usage().stream() .filter(row -> "token".equals(row.bucket())) .mapToLong(row -> row.calls()) .sum(); } } ``` **REST, Kotlin** ```kotlin // How many token calls the app made today, for your own alerts. package examples.apiusage.kotlin import org.atonnetwork.atheron.AtheronApi fun tokenCallsToday(apiKey: String): Long = AtheronApi(apiKey).usage(1).usage().filter { it.bucket() == "token" }.sumOf { it.calls() } ``` **REST, C#** ```csharp // How many token calls the app made today, for your own alerts. using AtonNetwork; namespace Examples.ApiUsage; public static class TokenCallsToday { public static async Task RunAsync(string apiKey) { var usage = await new AtheronApi(apiKey).UsageAsync(days: 1); return usage.Usage?.Where(row => row.Bucket == "token").Sum(row => row.Calls ?? 0) ?? 0; } } ``` **REST, PHP** ```php usage(days: 1)['usage']; return array_sum(array_column(array_filter($rows, fn (array $row) => $row['bucket'] === 'token'), 'calls')); } ``` **REST, Ruby** ```ruby # Today's token calls, for your own alert before you reach a daily limit. require "aton_network" def token_calls_today(api_key) AtonNetwork::Api.new(api_key).usage(days: 1)["usage"].select { |row| row["bucket"] == "token" }.sum { |row| row["calls"] } end ``` **REST, Elixir** ```elixir # Today's token calls, for your own alert before you reach a daily limit. defmodule MyApp.Usage do def token_calls_today(api_key) do with {:ok, %{"usage" => rows}} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.usage(days: 1) do {:ok, rows |> Enum.filter(&(&1["bucket"] == "token")) |> Enum.map(& &1["calls"]) |> Enum.sum()} end end end ``` **GraphQL, cURL** ```bash # Calls, errors and latency per day and bucket for the last seven days. Needs usage:read; costs 1. curl https://developers.aton-network.org/api/graphql \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"query Usage($days: Int = 7) {\n usage(days: $days) {\n env\n days\n usage {\n day\n bucket\n calls\n errors\n msAvg\n msMax\n }\n }\n}\n","variables":{"days":7}}' ``` **GraphQL, TypeScript** ```ts // Today's token calls, for your own alert before you reach a daily limit. const USAGE = `query Usage($days: Int = 7) { usage(days: $days) { env days usage { day bucket calls errors msAvg msMax } } } `; export async function tokenCallsToday(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/graphql", { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, body: JSON.stringify({ query: USAGE, variables: { days: 1 } }), }); const { data, errors } = (await answer.json()) as { data?: { usage: { usage: { bucket: string; calls: number }[] } | null }; errors?: { message: string; extensions: { code: string } }[] }; if (errors?.length) throw new Error(`${errors[0]!.extensions.code}: ${errors[0]!.message}`); return data!.usage!.usage.filter((row) => row.bucket === "token").reduce((sum, row) => sum + row.calls, 0); } ``` **GraphQL, Python** ```python # Today's token calls, for your own alert before you reach a daily limit. import os import requests USAGE = """query Usage($days: Int = 7) { usage(days: $days) { env days usage { day bucket calls errors msAvg msMax } } } """ def token_calls_today(api_key: str) -> int: answer = requests.post( "https://developers.aton-network.org/api/graphql", headers={"Authorization": f"Bearer {api_key}"}, json={"query": USAGE, "variables": {"days": 1}}, timeout=10, ) body = answer.json() if body.get("errors"): error = body["errors"][0] raise RuntimeError(f"{error['extensions']['code']}: {error['message']}") return sum(row["calls"] for row in body["data"]["usage"]["usage"] if row["bucket"] == "token") if __name__ == "__main__": print(token_calls_today(os.environ["ATHERON_API_KEY"])) ``` ### Your app and today's usage in one request **GraphQL, cURL** ```bash # Your app and today's usage in one request: two REST calls' worth, so it costs 2. # Persisted: send the hash alone once the document is known (see the persisted queries guide). curl https://developers.aton-network.org/api/graphql \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"query Overview {\n app {\n name\n state\n quota\n }\n usage(days: 1) {\n usage {\n bucket\n calls\n errors\n }\n }\n}\n"}' ``` **GraphQL, TypeScript** ```ts // A status line for your own dashboard: the app's state and today's calls, in one request. // A key with only one of app:read and usage:read gets that half, and an error for the other. const OVERVIEW = `query Overview { app { name state quota } usage(days: 1) { usage { bucket calls errors } } } `; export async function statusLine(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/graphql", { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, body: JSON.stringify({ query: OVERVIEW }), }); if (answer.status === 429) throw new Error(`over quota, retry in ${answer.headers.get("retry-after")} s`); const { data } = (await answer.json()) as { data?: { app: { name: string; state: string } | null; usage: { usage: { calls: number; errors: number }[] } | null }; }; const calls = data?.usage?.usage.reduce((sum, row) => sum + row.calls, 0) ?? 0; return `${data?.app?.name ?? "?"} (${data?.app?.state ?? "?"}): ${calls} calls today`; } ``` **GraphQL, Python** ```python # A status line for your own dashboard: the app's state and today's calls, in one request. import os import requests OVERVIEW = """query Overview { app { name state quota } usage(days: 1) { usage { bucket calls errors } } } """ def status_line(api_key: str) -> str: answer = requests.post( "https://developers.aton-network.org/api/graphql", headers={"Authorization": f"Bearer {api_key}"}, json={"query": OVERVIEW}, timeout=10, ) if answer.status_code == 429: raise RuntimeError(f"over quota, retry in {answer.headers.get('Retry-After')} s") data = answer.json().get("data") or {} app = data.get("app") or {} calls = sum(row["calls"] for row in (data.get("usage") or {}).get("usage", [])) return f"{app.get('name', '?')} ({app.get('state', '?')}): {calls} calls today" if __name__ == "__main__": print(status_line(os.environ["ATHERON_API_KEY"])) ``` ### Hear events as they happen **GraphQL, cURL** ```bash # curl does not speak WebSocket. The same events come as Server-Sent Events: -N keeps the stream open. # Needs events:read. Each event: id, event (its type) and data (the body a webhook would get). curl -N "https://developers.aton-network.org/api/v1/events?types=user.signed_in,consent.revoked" \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **GraphQL, TypeScript** ```ts // Hear sign-ins and withdrawn consent as they happen, over GraphQL (graphql-transport-ws), with the // ws package. Keep the last id you handled and pass it as after when you reconnect: nothing is missed. import WebSocket from "ws"; const EVENTS = `subscription Events($types: [EventType!], $after: ID) { events(types: $types, after: $after) { id type created clientId data { sub scopes at } } } `; export interface AtheronEvent { id: string; type: string; created: string; clientId: string; data: { sub: string | null; scopes: string[] | null; at: string | null }; } export function listen(apiKey: string, onEvent: (event: AtheronEvent) => void, after: string | null = null): () => void { const socket = new WebSocket("wss://developers.aton-network.org/api/graphql/ws", "graphql-transport-ws"); socket.on("open", () => socket.send(JSON.stringify({ type: "connection_init", payload: { authorization: `Bearer ${apiKey}` } }))); socket.on("message", (raw) => { const message = JSON.parse(raw.toString()) as { type: string; payload?: { data?: { events: AtheronEvent }; errors?: unknown } }; if (message.type === "connection_ack") { socket.send(JSON.stringify({ type: "subscribe", id: "events", payload: { query: EVENTS, variables: { types: ["USER_SIGNED_IN", "CONSENT_REVOKED"], after } } })); } else if (message.type === "next" && message.payload?.data) { onEvent(message.payload.data.events); } else if (message.type === "ping") { socket.send(JSON.stringify({ type: "pong" })); } else if (message.type === "error" || message.payload?.errors) { console.error("the subscription was refused", message.payload); } }); return () => socket.close(1000); } ``` **GraphQL, Python** ```python # Hear sign-ins and withdrawn consent as they happen, over GraphQL (graphql-transport-ws), with the # websockets package. Keep the last id you handled and pass it as after when you reconnect. import asyncio import json import os import websockets EVENTS = """subscription Events($types: [EventType!], $after: ID) { events(types: $types, after: $after) { id type created clientId data { sub scopes at } } } """ async def listen(api_key: str, after: str | None = None) -> None: async with websockets.connect("wss://developers.aton-network.org/api/graphql/ws", subprotocols=["graphql-transport-ws"]) as socket: await socket.send(json.dumps({"type": "connection_init", "payload": {"authorization": f"Bearer {api_key}"}})) async for raw in socket: message = json.loads(raw) if message["type"] == "connection_ack": variables = {"types": ["USER_SIGNED_IN", "CONSENT_REVOKED"], "after": after} await socket.send(json.dumps({"type": "subscribe", "id": "events", "payload": {"query": EVENTS, "variables": variables}})) elif message["type"] == "next": event = message["payload"].get("data", {}).get("events") if event: print(event["type"], event["data"]["sub"]) elif message["type"] == "ping": await socket.send(json.dumps({"type": "pong"})) elif message["type"] == "error": raise RuntimeError(message["payload"]) if __name__ == "__main__": asyncio.run(listen(os.environ["ATHERON_API_KEY"])) ``` ## Limits | Limit | Value | |---|---| | Request body | 16,384 bytes | | Tokens in a document | 2,000 | | Depth | 5 (introspection 15) | | Fields selected, aliases and fragments counted | 300 | | Cost | 25 (introspection 5) | | Variables | 4,096 bytes | | Operations in a request | 1; batches are refused | | Subscriptions on a socket | 5 | | Event streams per app | 10 | ## Errors Errors carry `extensions.code`, the same code the REST API uses where there is one. | code | REST code | HTTP | |---|---|---| | [`invalid_key`](#invalid-key) | invalid_key | 401 | | [`missing_right`](#missing-right) | missing_right | 200, beside the data | | [`not_found`](#not-found) | not_found | 200, beside the data | | [`rate_limited`](#rate-limited) | rate_limited | 429 | | [`invalid_request`](#invalid-request) | invalid_request | 400 | | [`query_too_complex`](#query-too-complex) | none | 400 | | [`persisted_query_not_found`](#persisted-query-not-found) | none | 200 | | [`server_error`](#server-error) | server_error | 200, beside the data | ### invalid_key **Cause:** No API key was sent, or it is not one of ours, has expired or was revoked. **Fix:** Send a live key as Authorization: Bearer ath_test_... or ath_live_... (over WebSocket, in connection_init's payload). ### missing_right **Cause:** The key does not carry the right a field needs. That field is null; the rest of the answer is there. **Fix:** Make a key with the right (app:read, usage:read, events:read) in the portal, or ask only for what this key may read. ### not_found **Cause:** The application the key belongs to has been removed, or an events after id is not this app's or is older than 30 days. **Fix:** Use a key of an app that still exists; resume from an id you were sent within 30 days, or start without after. ### rate_limited **Cause:** Over your api quota for this minute or day. A request takes its cost from the quota; too many open event streams also answer this. **Fix:** Wait the seconds in Retry-After. Ask for fewer fields that reach the database, or fewer aliases. ### invalid_request **Cause:** The request could not run: not JSON, a batch (an array), a syntax error, a field or argument the schema does not have, more than one operation without operationName, or a subscription sent over HTTP. **Fix:** Check the message; the schema is at /graphql/schema.graphql. Send one operation per request; subscriptions go over the WebSocket. ### query_too_complex **Cause:** Over a limit: depth, fields selected (aliases and fragments counted where they appear), introspection depth, or cost. **Fix:** Split the request, or ask for less in one go. The limits are listed on this page. ### persisted_query_not_found **Cause:** A persisted query's hash is not one we know yet (message PersistedQueryNotFound). **Fix:** Send the same request again with the query as well as the hash; it is then kept, and the hash alone works. ### server_error **Cause:** Something went wrong at our end. Nothing about it is shown. **Fix:** Try again in a moment. If it keeps happening, tell us on the support page with the time. --- # Events stream URL: https://developers.aton-network.org/docs/reference/events Every webhook event as it happens, over Server-Sent Events, a WebSocket or a GraphQL subscription. The same events your webhooks hear, as they happen, over a connection you keep open: no public address needed, in order, resumable for 30 days. Needs an API key with the `events:read` right. The [AsyncAPI description](https://developers.aton-network.org/asyncapi.json) describes all of it. ## Server-Sent Events `GET https://developers.aton-network.org/api/v1/events` A text/event-stream response that stays open. Each event: id: , event: , data: the webhook body. Resume with Last-Event-ID or ?after=. A comment every 15 seconds keeps proxies from closing it. ?types=user.signed_in,consent.revoked filters. **GraphQL, cURL** ```bash # The events stream as Server-Sent Events. Needs events:read. -N keeps it open. # Resume after the last event you handled: add -H "Last-Event-ID: " (kept 30 days). curl -N "https://developers.aton-network.org/api/v1/events?types=user.signed_in,consent.revoked" \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **GraphQL, TypeScript** ```ts // Read the events stream with fetch: each event's data is exactly the body a webhook would get. // Pass the last id you handled to resume; the stream sends what you missed first. export interface WebhookBody { id: string; type: string; created: string; client_id: string; data: Record; } export async function followEvents(apiKey: string, onEvent: (event: WebhookBody) => void, lastId: string | null = null, signal?: AbortSignal): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/events?types=user.signed_in,consent.revoked", { headers: { authorization: `Bearer ${apiKey}`, accept: "text/event-stream", ...(lastId ? { "last-event-id": lastId } : {}) }, ...(signal ? { signal } : {}), }); if (!answer.ok || !answer.body) throw new Error(`the events stream answered ${answer.status}`); const decoder = new TextDecoder(); let buffered = ""; for await (const chunk of answer.body as unknown as AsyncIterable) { buffered += decoder.decode(chunk, { stream: true }); let end: number; while ((end = buffered.indexOf("\n\n")) >= 0) { const block = buffered.slice(0, end); buffered = buffered.slice(end + 2); const event = /^event: (.+)$/m.exec(block)?.[1]; const data = /^data: (.+)$/m.exec(block)?.[1]; if (event === "error") throw new Error(`the events stream ended: ${data}`); if (data && event && event !== "ready") onEvent(JSON.parse(data) as WebhookBody); } } } ``` **GraphQL, Python** ```python # Read the events stream: each event's data is exactly the body a webhook would get. import json import os import requests def follow_events(api_key: str, last_id: str | None = None) -> None: headers = {"Authorization": f"Bearer {api_key}", "Accept": "text/event-stream"} if last_id: headers["Last-Event-ID"] = last_id with requests.get( "https://developers.aton-network.org/api/v1/events", params={"types": "user.signed_in,consent.revoked"}, headers=headers, stream=True, timeout=(10, 60), ) as answer: answer.raise_for_status() event = None for line in answer.iter_lines(decode_unicode=True): if line.startswith("event: "): event = line[7:] elif line.startswith("data: ") and event not in (None, "ready"): body = json.loads(line[6:]) if event == "error": raise RuntimeError(body) print(body["type"], body["data"].get("sub")) elif line == "": event = None if __name__ == "__main__": follow_events(os.environ["ATHERON_API_KEY"]) ``` ## WebSocket `WebSocket wss://developers.aton-network.org/api/v1/events/ws` Authorization: Bearer on the upgrade request. Frames are JSON: {"type":"ready"}, then {"type":"event","event":}; {"type":"error","error":,"error_description":...} before it closes. The same ?types= and ?after=. **GraphQL, TypeScript** ```ts // The events stream over a plain WebSocket, with the ws package. The key goes on the upgrade. import WebSocket from "ws"; export function followEventsOverSocket(apiKey: string, onEvent: (event: { id: string; type: string; data: Record }) => void): () => void { const socket = new WebSocket("wss://developers.aton-network.org/api/v1/events/ws?types=user.signed_in", { headers: { authorization: `Bearer ${apiKey}` }, }); socket.on("message", (raw) => { const frame = JSON.parse(raw.toString()) as { type: string; event?: { id: string; type: string; data: Record }; error?: string }; if (frame.type === "event" && frame.event) onEvent(frame.event); else if (frame.type === "error") console.error("the events stream ended:", frame.error); }); return () => socket.close(1000); } ``` **GraphQL, Python** ```python # The events stream over a plain WebSocket, with the websockets package. The key goes on the upgrade. import asyncio import json import os import websockets async def follow_events(api_key: str) -> None: async with websockets.connect( "wss://developers.aton-network.org/api/v1/events/ws?types=user.signed_in", additional_headers={"Authorization": f"Bearer {api_key}"}, ) as socket: async for raw in socket: frame = json.loads(raw) if frame["type"] == "event": print(frame["event"]["type"], frame["event"]["data"].get("sub")) elif frame["type"] == "error": raise RuntimeError(frame) if __name__ == "__main__": asyncio.run(follow_events(os.environ["ATHERON_API_KEY"])) ``` ## GraphQL subscription `graphql-transport-ws wss://developers.aton-network.org/api/graphql/ws` subscription { events(types, after) { ... } } over the graphql-transport-ws protocol, the key in connection_init's payload as authorization. **GraphQL, cURL** ```bash # curl does not speak WebSocket. The same events come as Server-Sent Events: -N keeps the stream open. # Needs events:read. Each event: id, event (its type) and data (the body a webhook would get). curl -N "https://developers.aton-network.org/api/v1/events?types=user.signed_in,consent.revoked" \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **GraphQL, TypeScript** ```ts // Hear sign-ins and withdrawn consent as they happen, over GraphQL (graphql-transport-ws), with the // ws package. Keep the last id you handled and pass it as after when you reconnect: nothing is missed. import WebSocket from "ws"; const EVENTS = `subscription Events($types: [EventType!], $after: ID) { events(types: $types, after: $after) { id type created clientId data { sub scopes at } } } `; export interface AtheronEvent { id: string; type: string; created: string; clientId: string; data: { sub: string | null; scopes: string[] | null; at: string | null }; } export function listen(apiKey: string, onEvent: (event: AtheronEvent) => void, after: string | null = null): () => void { const socket = new WebSocket("wss://developers.aton-network.org/api/graphql/ws", "graphql-transport-ws"); socket.on("open", () => socket.send(JSON.stringify({ type: "connection_init", payload: { authorization: `Bearer ${apiKey}` } }))); socket.on("message", (raw) => { const message = JSON.parse(raw.toString()) as { type: string; payload?: { data?: { events: AtheronEvent }; errors?: unknown } }; if (message.type === "connection_ack") { socket.send(JSON.stringify({ type: "subscribe", id: "events", payload: { query: EVENTS, variables: { types: ["USER_SIGNED_IN", "CONSENT_REVOKED"], after } } })); } else if (message.type === "next" && message.payload?.data) { onEvent(message.payload.data.events); } else if (message.type === "ping") { socket.send(JSON.stringify({ type: "pong" })); } else if (message.type === "error" || message.payload?.errors) { console.error("the subscription was refused", message.payload); } }); return () => socket.close(1000); } ``` **GraphQL, Python** ```python # Hear sign-ins and withdrawn consent as they happen, over GraphQL (graphql-transport-ws), with the # websockets package. Keep the last id you handled and pass it as after when you reconnect. import asyncio import json import os import websockets EVENTS = """subscription Events($types: [EventType!], $after: ID) { events(types: $types, after: $after) { id type created clientId data { sub scopes at } } } """ async def listen(api_key: str, after: str | None = None) -> None: async with websockets.connect("wss://developers.aton-network.org/api/graphql/ws", subprotocols=["graphql-transport-ws"]) as socket: await socket.send(json.dumps({"type": "connection_init", "payload": {"authorization": f"Bearer {api_key}"}})) async for raw in socket: message = json.loads(raw) if message["type"] == "connection_ack": variables = {"types": ["USER_SIGNED_IN", "CONSENT_REVOKED"], "after": after} await socket.send(json.dumps({"type": "subscribe", "id": "events", "payload": {"query": EVENTS, "variables": variables}})) elif message["type"] == "next": event = message["payload"].get("data", {}).get("events") if event: print(event["type"], event["data"]["sub"]) elif message["type"] == "ping": await socket.send(json.dumps({"type": "pong"})) elif message["type"] == "error": raise RuntimeError(message["payload"]) if __name__ == "__main__": asyncio.run(listen(os.environ["ATHERON_API_KEY"])) ``` ## Which to use Webhooks when your server has a public address and you want us to retry for a day. The events stream for a laptop, a worker behind a firewall, or a dashboard that shows sign-ins as they happen. The [atheron CLI](/docs/sdks/cli) forwards test events to localhost. --- # Errors URL: https://developers.aton-network.org/docs/errors Every error code we return, where, what causes it and how to fix it. Errors follow OAuth 2.0: a JSON body `{ "error": "...", "error_description": "..." }`, or, at the authorize step, `error` and `error_description` on the redirect back to you. Read `error` in code and show `error_description` only in your logs. Some problems at the authorize step are shown on our page and never sent back: an unknown client, a redirect address you did not register, and a repeated parameter. We never send anybody to an address we have not checked. | error | HTTP | Where | |---|---|---| | [`access_denied`](#access-denied) | on the redirect | [Authorize](/docs/reference/authorize) | | [`invalid_request`](#invalid-request) | 400 | [Authorize](/docs/reference/authorize), [Token: swap a code](/docs/reference/token), [Token: refresh](/docs/reference/refresh), [Revoke](/docs/reference/revoke) | | [`invalid_scope`](#invalid-scope) | on the redirect | [Authorize](/docs/reference/authorize) | | [`unsupported_response_type`](#unsupported-response-type) | on the redirect | [Authorize](/docs/reference/authorize) | | [`login_required`](#login-required) | on the redirect | [Authorize](/docs/reference/authorize) | | [`consent_required`](#consent-required) | on the redirect | [Authorize](/docs/reference/authorize) | | [`interaction_required`](#interaction-required) | on the redirect | [Authorize](/docs/reference/authorize) | | [`invalid_client`](#invalid-client) | 401 | [Token: swap a code](/docs/reference/token), [Token: refresh](/docs/reference/refresh), [Revoke](/docs/reference/revoke) | | [`invalid_grant`](#invalid-grant) | 400 | [Token: swap a code](/docs/reference/token), [Token: refresh](/docs/reference/refresh) | | [`unsupported_grant_type`](#unsupported-grant-type) | 400 | [Token: swap a code](/docs/reference/token) | | [`invalid_token`](#invalid-token) | 401 | [Userinfo](/docs/reference/userinfo) | | [`rate_limited`](#rate-limited) | 429 | [Token: swap a code](/docs/reference/token), [Token: refresh](/docs/reference/refresh), [Userinfo](/docs/reference/userinfo), [Developer API: your app](/docs/reference/api-app), [Developer API: usage](/docs/reference/api-usage), [MCP server](/docs/reference/mcp) | | [`invalid_key`](#invalid-key) | 401 | [Developer API: your app](/docs/reference/api-app), [Developer API: usage](/docs/reference/api-usage), [MCP server](/docs/reference/mcp) | | [`missing_right`](#missing-right) | 403 | [Developer API: your app](/docs/reference/api-app), [Developer API: usage](/docs/reference/api-usage) | | [`not_found`](#not-found) | 404 | [Developer API: your app](/docs/reference/api-app) | | [`server_error`](#server-error) | 500 | [Developer API: your app](/docs/reference/api-app), [Developer API: usage](/docs/reference/api-usage) | ## access_denied **Cause:** The person chose not now, or your app may not sign them in yet: a test app signs in only its team and named testers, a live app only after review, and nothing while it is suspended. **Fix:** Show a way to try again. Read error_description to tell the cases apart, and add testers on the app's Testers page. ## invalid_request **Cause:** A parameter is missing or malformed: no PKCE, PKCE with plain, a bad max_age, prompt=none combined with another prompt, or a token request that is not form encoded. **Fix:** Send every required parameter once, PKCE with S256, and form-encode the token request. ## invalid_scope **Cause:** A scope does not exist, or your client is not registered for it. **Fix:** Add the scope on the app's Sign-in page. A sensitive scope on the live app needs a review first. ## unsupported_response_type **Cause:** response_type is not code. **Fix:** Use the authorization code flow. ## login_required **Cause:** prompt=none was sent and nobody is signed in, or their sign-in is older than max_age. **Fix:** Send them again without prompt=none. ## consent_required **Cause:** prompt=none was sent and the person has not agreed to these scopes yet. **Fix:** Send them again without prompt=none. ## interaction_required **Cause:** prompt=none was sent and their account needs a step first, such as accepting new terms. **Fix:** Send them again without prompt=none. ## invalid_client **Cause:** Your app could not be authenticated: a wrong or expired secret, a revoked or expired certificate, a bad client_assertion (aud must be the token endpoint, exp at most ten minutes ahead, jti new), a public app sending a secret, or the app is suspended. **Fix:** Check the credential on the app's Credentials page and its expiry. Rotate without downtime: add a new one, deploy, then revoke the old. ## invalid_grant **Cause:** The code or refresh token cannot be used: the code expired (60 seconds) or was used, it was issued to another client, the redirect_uri or code_verifier does not match, a refresh token was used twice, access was withdrawn, or the tester was removed. **Fix:** Start the sign-in again. Swap each code once, straight away, with the same redirect_uri and the verifier you kept. ## unsupported_grant_type **Cause:** grant_type is not authorization_code or refresh_token. **Fix:** Use one of the two. ## invalid_token **Cause:** Not an access token (ID tokens are refused), expired (15 minutes), the account is suspended, access was withdrawn, or the app is suspended. **Fix:** Refresh the access token, or sign the person in again. Never send an ID token to userinfo. ## rate_limited **Cause:** Over your quota for this bucket, this minute or this day. **Fix:** Wait the Retry-After seconds. Requests over the limit still count, so a retry loop keeps you over it. ## invalid_key **Cause:** No API key was sent, or it is not one of ours, expired or revoked. **Fix:** Send Authorization: Bearer with a key from the app's API keys page. ## missing_right **Cause:** The key does not carry the right this call needs. **Fix:** Make a key with the right (app:read or usage:read). ## not_found **Cause:** The application this key belonged to has been removed. **Fix:** Use a key of an app that still exists. ## server_error **Cause:** Something went wrong at our end. **Fix:** Try again in a moment. If it keeps happening, check the status page and tell us in the chat. --- # Webhooks URL: https://developers.aton-network.org/docs/webhooks The events we send, their payloads, how they are signed and how retries work. Add a webhook for the test or live environment on your app's Webhooks page. We send a POST with a JSON body to your https address, signed with your signing secret. ## Events | type | When | |---|---| | [`user.signed_in`](/docs/webhooks/user.signed_in) | Somebody signed in to your app: a code was issued to your client. | | [`consent.revoked`](/docs/webhooks/consent.revoked) | Somebody took back your access, from their account or any other way, or we suspended your app. | | [`membership.changed`](/docs/webhooks/membership.changed) | The plan of somebody who granted you the membership scope changed. | | [`ping`](/docs/webhooks/ping) | You pressed Send a test on the app's Webhooks page. | ## Headers | Header | What it is | |---|---| | `Atheron-Signature` | t=,v1=." with your signing secret>. | | `Atheron-Event-Id` | The event's id. Keep the ones you handled and ignore a repeat. | | `Content-Type` | application/json. | ## Checking the signature The hex is HMAC-SHA256 of `.` with your signing secret (`whsec_...`, shown once). Use the raw body, exactly as received, and refuse anything older than five minutes. **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## Retries Answer with any 2xx within ten seconds. Anything else is tried again after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours, then we give up. We never follow redirects, and never deliver to a private or loopback address. Use `Atheron-Event-Id` to ignore a delivery you already handled. ## Sending a delivery again When your endpoint was down, send past deliveries again from the app's **Webhooks** tab (one, or every one that gave up) or with `atheron webhooks replay`. A resend is the original delivery as it was, the same body and the same `Atheron-Event-Id`, signed at the moment it goes, so the handler that drops repeats keeps working. Each resend is tried once. You can resend deliveries from the last thirty days, up to 25 at a time and 60 an hour for an app; live deliveries need the owner or admin role, and every resend shows in the app's Activity. A full walk through is in the [webhooks tutorial](/docs/guides/webhooks). --- # Event: user.signed_in URL: https://developers.aton-network.org/docs/webhooks/user.signed_in Somebody signed in to your app: a code was issued to your client. Somebody signed in to your app: a code was issued to your client. ## Payload ```json { "id": "6f0c2e1a-8b0d-4f4e-9a51-2c1d7e9b3a10", "type": "user.signed_in", "created": "2026-10-15T17:02:11.000Z", "client_id": "live-1a2b3c4d5e6f7a8b", "data": { "sub": "9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11", "scopes": ["openid", "profile", "email"], "at": "2026-10-15T17:02:11.000Z" } } ``` | Field | Type | What it is | |---|---|---| | `id` | string | The event's id, the same as the Atheron-Event-Id header | | `type` | string | `user.signed_in` | | `created` | string (ISO 8601) | When the event was raised | | `client_id` | string | The client (test or live) it is about | | `data.sub` | string | The person's ID for your app alone (pairwise): the sub your app was given. | | `data.scopes` | string[] | The scopes granted for this sign-in. | | `data.at` | string (ISO 8601) | When it happened. | ## What to do Nothing is required. Use it for your own analytics or to warm a cache. ## Checking it Check the signature before you act on it ([how](/docs/webhooks#checking-the-signature)), and answer 2xx within ten seconds. --- # Event: consent.revoked URL: https://developers.aton-network.org/docs/webhooks/consent.revoked Somebody took back your access, from their account or any other way, or we suspended your app. Somebody took back your access, from their account or any other way, or we suspended your app. ## Payload ```json { "id": "6f0c2e1a-8b0d-4f4e-9a51-2c1d7e9b3a10", "type": "consent.revoked", "created": "2026-10-15T17:02:11.000Z", "client_id": "live-1a2b3c4d5e6f7a8b", "data": { "sub": "9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11", "at": "2026-10-15T17:02:11.000Z" } } ``` | Field | Type | What it is | |---|---|---| | `id` | string | The event's id, the same as the Atheron-Event-Id header | | `type` | string | `consent.revoked` | | `created` | string (ISO 8601) | When the event was raised | | `client_id` | string | The client (test or live) it is about | | `data.sub` | string | The person's ID for your app alone (pairwise): the sub your app was given. | | `data.at` | string (ISO 8601) | When access ended. | ## What to do Delete the refresh token you hold for them and stop using what you read, as the data-use policy says. ## Checking it Check the signature before you act on it ([how](/docs/webhooks#checking-the-signature)), and answer 2xx within ten seconds. --- # Event: membership.changed URL: https://developers.aton-network.org/docs/webhooks/membership.changed The plan of somebody who granted you the membership scope changed. The plan of somebody who granted you the membership scope changed. ## Payload ```json { "id": "6f0c2e1a-8b0d-4f4e-9a51-2c1d7e9b3a10", "type": "membership.changed", "created": "2026-10-15T17:02:11.000Z", "client_id": "live-1a2b3c4d5e6f7a8b", "data": { "sub": "9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11", "at": "2026-10-15T17:02:11.000Z" } } ``` | Field | Type | What it is | |---|---|---| | `id` | string | The event's id, the same as the Atheron-Event-Id header | | `type` | string | `membership.changed` | | `created` | string (ISO 8601) | When the event was raised | | `client_id` | string | The client (test or live) it is about | | `data.sub` | string | The person's ID for your app alone (pairwise): the sub your app was given. | | `data.at` | string (ISO 8601) | When it changed. | ## What to do Call userinfo with a fresh access token to read the new plan. The event never carries the plan itself. ## Checking it Check the signature before you act on it ([how](/docs/webhooks#checking-the-signature)), and answer 2xx within ten seconds. --- # Event: ping URL: https://developers.aton-network.org/docs/webhooks/ping You pressed Send a test on the app's Webhooks page. You pressed Send a test on the app's Webhooks page. ## Payload ```json { "id": "6f0c2e1a-8b0d-4f4e-9a51-2c1d7e9b3a10", "type": "ping", "created": "2026-10-15T17:02:11.000Z", "client_id": "live-1a2b3c4d5e6f7a8b", "data": { "at": "2026-10-15T17:02:11.000Z" } } ``` | Field | Type | What it is | |---|---|---| | `id` | string | The event's id, the same as the Atheron-Event-Id header | | `type` | string | `ping` | | `created` | string (ISO 8601) | When the event was raised | | `client_id` | string | The client (test or live) it is about | | `data.at` | string (ISO 8601) | When it was sent. | ## What to do Answer 2xx. It proves your address, your signature check and your firewall. ## Checking it Check the signature before you act on it ([how](/docs/webhooks#checking-the-signature)), and answer 2xx within ten seconds. --- # SDKs and libraries URL: https://developers.aton-network.org/docs/sdks Every Atheron SDK and framework integration, what is out and what is coming. Every SDK is generated from one OpenAPI description of our API, with hand-written sign-in and webhook helpers, and the same behaviour checked across languages by one shared test suite. Until the SDK for your language is out, everything works over plain HTTP: every page here has cURL examples, and any OpenID Connect library reads our [discovery document](/docs/reference/discovery). Pick your language at the top of any code block and the whole site follows it. ## JavaScript and TypeScript | SDK | Status | Package | Frameworks | |---|---|---|---| | [JavaScript and TypeScript](/docs/sdks/javascript) | Coming soon | `@aton-network/sdk` (npm) | Node and the browser, Next.js, React, Vue and Nuxt, SvelteKit, Angular, Express, NestJS, React Router and Remix, Auth.js provider, Passport strategy | ## Server languages | SDK | Status | Package | Frameworks | |---|---|---|---| | [Python](/docs/sdks/python) | Coming soon | `aton-network` (PyPI) | Django, Flask, FastAPI, django-allauth | | [Rust](/docs/sdks/rust) | Coming soon | `aton-network` (crates.io) | Axum, Actix Web | | [Go](/docs/sdks/go) | Coming soon | `github.com/Atheron-ATON-Network/atheron-sdk-go` (Go modules) | net/http, Gin, Echo | | [Java and Kotlin](/docs/sdks/java) | Coming soon | `org.aton-network:aton-network-sdk` (Maven Central) | Spring Boot, Spring Security, Kotlin | | [C# and .NET](/docs/sdks/dotnet) | Coming soon | `AtonNetwork.Sdk` (NuGet) | ASP.NET Core | | [PHP](/docs/sdks/php) | Coming soon | `aton-network/sdk` (Packagist) | Laravel Socialite, Symfony, WordPress plugin | | [Ruby](/docs/sdks/ruby) | Coming soon | `aton-network` (RubyGems) | Rails, OmniAuth strategy | | [Elixir](/docs/sdks/elixir) | Coming soon | `aton_network` (Hex) | Phoenix | ## Mobile and desktop | SDK | Status | Package | Frameworks | |---|---|---|---| | Swift | Planned | `atheron-swift` (Swift Package Manager) | Native | | Kotlin for Android | Planned | `org.aton-network:aton-network-android` (Maven Central) | Native | | Flutter and Dart | Planned | `aton_network` (pub.dev) | Native | | React Native and Expo | Planned | `@aton-network/react-native` (npm) | Native | | .NET MAUI | Planned | `AtonNetwork.Maui` (NuGet) | Native | | Electron | Planned | `@aton-network/electron` (npm) | Native | | Tauri | Planned | `tauri-plugin-aton-network` (crates.io) | Native | ## Games and no-code | SDK | Status | Package | Frameworks | |---|---|---|---| | Unity | Planned | `com.atheron.sdk` (Unity package) | Native | | Unreal | Planned | `Atheron for Unreal` (Fab) | Native | | Godot | Planned | `Atheron` (Godot Asset Library) | Native | | Zapier | Planned | `Atheron` (Zapier) | Native | | Make | Planned | `Atheron` (Make) | Native | | n8n | Planned | `n8n-nodes-aton-network` (npm) | Native | ## The command line The `atheron` CLI signs you in, lists your apps, rotates secrets and forwards webhooks to localhost. See [the CLI](/docs/sdks/cli). --- # JavaScript and TypeScript SDK URL: https://developers.aton-network.org/docs/sdks/javascript Install and use @aton-network/sdk. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/sdk` is Sign in with Atheron for TypeScript and JavaScript: PKCE, the authorize address, the code swap, userinfo and webhook signatures. It uses Web Crypto only, so it runs in Node 20 and later, Deno, Bun, edge runtimes and browsers. ## Install ```bash npm install @aton-network/sdk ``` ## Sign somebody in Build the address on your server, keep `verifier`, `state` and `nonce` for this browser in an HTTP-only cookie, and redirect. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` Check the ID token with a JOSE library (for example `jwtVerify` with `createRemoteJWKSet` from jose) against `https://accounts.aton-network.org/.well-known/jwks.json`: signature, issuer, audience, expiry and nonce. This package does not reimplement signature checks. ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `AtheronGraphQL` runs typed queries: `query(UsageQuery, { days: 30 })` answers `usage` with its type, and `gql("query ...")` types your own. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `query()` throws an `AtheronGraphQLError` when any field failed, with the data that could be read on it; `request()` answers the data and the field errors side by side. `events()` and `subscribe()` run over the WebSocket (`graphql-transport-ws`), reconnect when the connection drops, resume after the last event handed on and stop on a refusal waiting cannot fix. Node 22 and browsers have `WebSocket`; on Node 20, pass `{ WebSocket }` from the `ws` package. ## Errors Every failure from our endpoints is thrown as `AtheronError`, with `code` (the OAuth error, for example `invalid_grant`), `description` and the HTTP `status`. See [Errors](/docs/errors) for every code. ## Reference | Export | What it does | |---|---| | `ISSUER` | `https://accounts.aton-network.org` | | `Scope` | The scope names the provider issues | | `pkcePair()` | A verifier and its S256 challenge | | `startSignIn({ clientId, redirectUri, scopes?, issuer?, prompt? })` | The authorize address and the verifier, state and nonce to keep | | `finishSignIn({ callbackUrl, kept, clientId, clientSecret?, redirectUri, issuer? })` | Checks state and swaps the code for `Tokens` | | `userinfo(accessToken, issuer?)` | The claims as of now | | `refreshTokens({ clientId, clientSecret?, refreshToken })`, `revokeToken({ clientId, clientSecret?, token, hint? })` | New tokens from a refresh token; a token revoked | | `idTokenClaims(idToken, { clientId, nonce })` | An ID token from the token endpoint, held to issuer, audience, expiry and nonce | | `verifyWebhook(header, rawBody, secret or [secrets], { toleranceSeconds?, now? })` | Whether a delivery is ours and fresh; two secrets while you rotate | | `constructEvent(header, rawBody, secret)` | The checked event, or `invalid_signature` | | `followEvents({ apiKey, types?, lastEventId?, signal? })` | The [events stream](/docs/reference/api-events) as an async iterator: reconnects, resumes after the last id, stops on a refusal waiting cannot fix | | `new AtheronGraphQL({ apiKey, baseUrl?, WebSocket?, persistedQueries? })` | GraphQL: `query(doc, vars)`, `request(doc, vars)`, `subscribe(doc, vars, options)`, `events({ types?, lastEventId?, signal? })` | | `AppQuery`, `UsageQuery`, `OverviewQuery`, `EventsSubscription`, `gql(document)` | The published documents, typed, and your own | | `createAtheron({ clientId, clientSecret?, redirectUri, secret })` | A web server's whole sign-in on Request and Response, with a sealed session cookie | | `AtheronApi({ apiKey })` | The developer API (`app()`, `usage({ days })`), generated from the [OpenAPI description](/openapi.json) | | `AtheronError` | What every call throws, with `code`, `description`, `status` and, on a 429, `retryAfter` | | `Tokens`, `Started`, `WebhookEvent`, `Session` | The shapes of what comes back | ## Frameworks [Next.js](/docs/sdks/javascript/nextjs), [React](/docs/sdks/javascript/react), [Vue and Nuxt](/docs/sdks/javascript/vue), [SvelteKit](/docs/sdks/javascript/sveltekit), [Angular](/docs/sdks/javascript/angular), [Express](/docs/sdks/javascript/express), [NestJS](/docs/sdks/javascript/nestjs), [React Router and Remix](/docs/sdks/javascript/react-router), [Auth.js](/docs/sdks/javascript/authjs), [Passport](/docs/sdks/javascript/passport), [Node and the browser](/docs/sdks/javascript/node), and the [atheron CLI](/docs/sdks/cli). --- # Node and the browser (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/node Sign in with Atheron in Node and the browser. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). In plain Node (20 or later), `@aton-network/sdk` is all you need: there is no framework glue. ```bash npm install @aton-network/sdk ``` ## Start **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## Refresh **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Sign out for good **cURL** ```bash # End a refresh token and every token in its line. Always answers 200 once your app is authenticated. curl https://accounts.aton-network.org/oauth/revoke \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d token="$REFRESH_TOKEN" \ -d token_type_hint=refresh_token ``` **TypeScript** ```ts // When somebody signs out of your app for good, end their refresh token. import { ISSUER } from "@aton-network/sdk"; export async function revokeRefreshToken(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/revoke`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token: refreshToken, token_type_hint: "refresh_token" }), }); if (!answer.ok) throw new Error(`revoke answered ${answer.status}`); } ``` **Python** ```python # When somebody disconnects your app: revoke their refresh token, then delete it. import os from aton_network import Client def revoke_refresh_token(refresh_token: str) -> None: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) client.revoke_token(refresh_token, "refresh_token") ``` **Rust** ```rust // Revoke a refresh token when somebody signs out or disconnects your app. use aton_network::{Client, Error}; pub async fn revoke_refresh_token(refresh_token: &str) -> Result<(), Error> { Client::from_env().revoke_token(refresh_token, Some("refresh_token")).await } ``` **Go** ```go // Revoke a refresh token when somebody signs out or disconnects your app. package revoke import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RevokeRefreshToken(ctx context.Context, refreshToken string) error { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RevokeToken(ctx, refreshToken, "refresh_token") } ``` **Java** ```java // Revoke a refresh token when somebody signs out or disconnects your app. package examples.revoke.java; import org.atonnetwork.atheron.AtheronClient; public class RevokeRefreshToken { public static void revokeRefreshToken(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); client.revokeToken(refreshToken, "refresh_token"); } } ``` **Kotlin** ```kotlin // Revoke a refresh token when somebody signs out or disconnects your app. package examples.revoke.kotlin import org.atonnetwork.atheron.AtheronClient fun revokeRefreshToken(refreshToken: String) { AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).revokeToken(refreshToken, "refresh_token") } ``` **C#** ```csharp // Revoke a refresh token when somebody signs out or disconnects your app. using AtonNetwork; namespace Examples.Revoke; public static class RevokeRefreshToken { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RevokeTokenAsync(refreshToken, "refresh_token"); } } ``` **PHP** ```php revokeToken($refreshToken, 'refresh_token'); } ``` **Ruby** ```ruby # When somebody signs out of your app for good, end their refresh token. require "aton_network" def revoke_refresh_token(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.revoke_token(refresh_token, "refresh_token") end ``` **Elixir** ```elixir # When somebody signs out of your app for good, end their refresh token. defmodule MyApp.Revoke do def revoke_refresh_token(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.revoke_token(refresh_token, "refresh_token") end end ``` The [Node quickstart](/docs/quickstart/node) shows a whole server. --- # Next.js (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/nextjs Sign in with Atheron in Next.js. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/nextjs` signs people in to a Next.js app (App Router): one route file for every sign-in route, the person in server components and server actions, and protection in `proxy.ts`. ## Install ```bash npm install @aton-network/nextjs ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Add the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` This answers `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` and `/auth/session`. Link to `/auth/sign-in?return_to=/account` to sign somebody in and bring them back. ## Protect pages **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # React (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/react Sign in with Atheron in React. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/react` gives React a provider that asks your server who is signed in, a `useAtheron()` hook and the brand kit's button in five languages. Pair it with a server package (Next.js, Express, React Router and the others), which answers `/auth/session`. ## Install ```bash npm install @aton-network/react ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Sign in and out **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Show something only to a signed-in person **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php ` takes `locale` (en, fr-CA, es-ES, de, ur-IN), `theme` (dark, light, auto), `size` (32, 40, 48) and `variant` (signIn, continue), as the [button kit](/docs/branding) describes. ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # Vue and Nuxt (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/vue Sign in with Atheron in Vue and Nuxt. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/vue` brings Sign in with Atheron to Vue 3 and Nuxt: a plugin, a `useAtheron()` composable, the `` component, and `@aton-network/vue/nuxt` for Nuxt's server routes. ## Install ```bash npm install @aton-network/vue ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Nuxt: the server routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Vue: the plugin and the person **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` Add `atheron: import("@aton-network/sveltekit").Session | null` to `App.Locals` in `src/app.d.ts`. ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # Angular (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/angular Sign in with Atheron in Angular. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/angular` gives Angular 17 and later a service, a guard and the button. Your server answers `/auth` with any Atheron server package. ## Install ```bash npm install @aton-network/angular ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Provide it and guard routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # Express (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/express Sign in with Atheron in Express. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/express` signs people in to Express 4 and 5, Connect and plain `node:http`: middleware that answers `/auth` and puts the person on every request, and `requireUser`. ## Install ```bash npm install @aton-network/express ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Add the middleware **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Require somebody signed in **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` The full address of a request is built from your redirect URI's origin, never from the Host header, which the browser controls. ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # NestJS (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/nestjs Sign in with Atheron in NestJS. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/nestjs` signs people in to NestJS on its Express platform: the middleware and a guard. It imports nothing from `@nestjs/*`, so it fits every version. ## Install ```bash npm install @aton-network/nestjs ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Wire it in main.ts **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` A request the guard refuses gets a 401. Read the person with `userOf(req)`. ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # React Router and Remix (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/react-router Sign in with Atheron in React Router and Remix. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/react-router` signs people in to React Router 7 (framework mode) and Remix 2: a splat route's loader and action, and `requireUser` for any loader. ## Install ```bash npm install @aton-network/react-router ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Add the splat route **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` `requireUser(request)` throws the redirect to sign in, which both frameworks follow. ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # Auth.js provider (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/authjs Sign in with Atheron in Auth.js provider. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/authjs` is Atheron as an Auth.js provider, for next-auth 5 and every Auth.js framework. Auth.js reads our discovery document and checks PKCE, state and nonce. ## Install ```bash npm install @aton-network/authjs ``` Set these on your server (never in code), or the AUTH_ATHERON names below: `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Add the provider **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` Register `https://your.app/api/auth/callback/atheron` as the redirect address. Auth.js reads `AUTH_ATHERON_ID` and `AUTH_ATHERON_SECRET` when you pass nothing. ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # Passport strategy (JavaScript and TypeScript) URL: https://developers.aton-network.org/docs/sdks/javascript/passport Sign in with Atheron in Passport strategy. **Coming soon.** `@aton-network/sdk` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `@aton-network/passport` is Sign in with Atheron as a Passport strategy, with PKCE, state and nonce kept in the session. Add express-session (or cookie-session) before Passport. ## Install ```bash npm install @aton-network/passport ``` Set these on your server (never in code): `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` (server apps), `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SECRET` (32 or more random characters for sealing cookies: `openssl rand -base64 32`). ## Use the strategy **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in, checked in that order when the browser comes back. - The flow and the session live in HTTP-only cookies sealed with AES-256-GCM, so the browser carries them but cannot read or change them. Tokens never reach your page's JavaScript. - Sign-out is a POST from your own site only, and revokes the refresh token at Atheron. - A return address is always a path on your own site. The same rules hold in every Atheron SDK: they all run one set of conformance tests. --- # Python SDK URL: https://developers.aton-network.org/docs/sdks/python Install and use aton-network. **Coming soon.** `aton-network` is not published on PyPI yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `atheron` is Sign in with Atheron for Python: PKCE, the authorize address, the code swap, userinfo, webhook signatures and the developer API client, on the standard library alone. Integrations for Django, Flask, FastAPI and django-allauth come with it. ## Install ```bash pip install aton-network # or aton-network[django], aton-network[flask], aton-network[fastapi], aton-network[allauth] ``` Python 3.9 and later. ## Sign somebody in Build the address on your server and keep `started.kept()` (the verifier, state and nonce) in their session. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `follow_events` reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without `events:read`. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `AtheronGraphQL` runs queries on the standard library: `query(USAGE_QUERY, {"days": 30})` answers a `TypedDict`, and `gql("query ...")` makes your own document. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `query()` raises `AtheronGraphQLError` when any field failed, with the data that could be read on it; `request()` answers the data and the field errors side by side. `events()` and `subscribe()` run over the WebSocket, reconnect, resume after the last event handed on and stop on a refusal waiting cannot fix; they need the `graphql` extra (`pip install "aton-network[graphql]"`) or a `connect=` of your own. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is raised as `AtheronError` with `code` (for example `invalid_grant`), `description`, the HTTP `status` and, on a 429, `retry_after`. See [Errors](/docs/errors) for every code. ## Frameworks [Django](/docs/sdks/python/django), [Flask](/docs/sdks/python/flask), [FastAPI](/docs/sdks/python/fastapi) and [django-allauth](/docs/sdks/python/allauth). --- # Django (Python) URL: https://developers.aton-network.org/docs/sdks/python/django Sign in with Atheron in Django. **Coming soon.** `aton-network` is not published on PyPI yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton_network.contrib.django` signs people in on Django's own sessions: views, URLs, a middleware and `@atheron_required`. Install with `pip install aton-network[django]`. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Add the URLs **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a view **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, and signing out revokes the refresh token at Atheron. --- # Flask (Python) URL: https://developers.aton-network.org/docs/sdks/python/flask Sign in with Atheron in Flask. **Coming soon.** `aton-network` is not published on PyPI yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton_network.contrib.flask` is a blueprint with the routes, `current_user()` and `@login_required`, on Flask's session. Install with `pip install aton-network[flask]` and set `app.secret_key`. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Register the blueprint **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, and signing out revokes the refresh token at Atheron. --- # FastAPI (Python) URL: https://developers.aton-network.org/docs/sdks/python/fastapi Sign in with Atheron in FastAPI. **Coming soon.** `aton-network` is not published on PyPI yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton_network.contrib.fastapi` is a router with the routes and the dependencies `current_user` and `require_user`, on Starlette's SessionMiddleware. Install with `pip install aton-network[fastapi]`. Calls to Atheron run in a worker thread, so the event loop never waits on them. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Include the router **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Require somebody signed in **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, and signing out revokes the refresh token at Atheron. --- # django-allauth (Python) URL: https://developers.aton-network.org/docs/sdks/python/allauth Sign in with Atheron in django-allauth. **Coming soon.** `aton-network` is not published on PyPI yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton_network.contrib.allauth` is Atheron as a django-allauth provider, with PKCE on. Install with `pip install aton-network[allauth]`. ## Settings ```python INSTALLED_APPS = [..., "allauth", "allauth.account", "allauth.socialaccount", "aton_network.contrib.allauth"] SOCIALACCOUNT_PROVIDERS = { "atheron": { "APPS": [{"client_id": env("ATHERON_CLIENT_ID"), "secret": env("ATHERON_CLIENT_SECRET")}], "SCOPE": ["openid", "profile", "email"], }, } ``` Register `https://your.app/accounts/atheron/login/callback/` as the redirect address. The account's email comes from Atheron only when it is verified; the person's ID (`sub`) is the account's UID. --- # Rust SDK URL: https://developers.aton-network.org/docs/sdks/rust Install and use aton-network. **Coming soon.** `aton-network` is not published on crates.io yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton-network` is Sign in with Atheron for Rust: PKCE, the authorize address, the code swap, userinfo, ID token checks, sessions, webhook signatures, the events stream and the developer API client. The [Axum](/docs/sdks/rust/axum) and [Actix Web](/docs/sdks/rust/actix) integrations are crates of their own, thin layers over the same core, and ship on 30 October 2026. ## Install ```bash cargo add aton-network # the core cargo add aton-network aton-network-axum # or aton-network-actix ``` Rust 1.88 and later. The core reaches us with reqwest and rustls; turn default features off to give a `Transport` of your own. ## Sign somebody in Build the address on your server and keep `started.kept()` (the verifier, state and nonce) in their session. In a web app, the Axum or Actix Web integration does this for you. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` The integrations' `Webhook` extractor reads the raw body (up to 1 MiB), checks the signature against every secret you hold (`ATHERON_WEBHOOK_SECRET`, and `ATHERON_WEBHOOK_SECRET_PREVIOUS` while you rotate), and answers 400 for a forgery before your handler runs. Answer 204 when you have handled it; any 5xx and we deliver it again. ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `follow_events` reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without `events:read`, when your handler answers `Next::Stop` or fails, or when you drop the future. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `GraphQl` runs typed queries: `gql.query::(&USAGE_QUERY, Some(json!({ "days": 30 })))`. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `query` answers the first field error when any field failed; `request` answers the data and the field errors side by side. The crate keeps to one HTTP client, so use `follow_events` for events as they happen, or a WebSocket crate of your own with `ws_close_action`, `ws_error_action` and `resume_variables`. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is an `aton_network::Error` with `code` (for example `invalid_grant`), `description`, the HTTP `status` and, on a 429, `retry_after` in seconds. See [Errors](/docs/errors) for every code. ## Reference | Item | What it does | |---|---| | `Auth::from_env(Config) -> Result` | Sign-in for a web app, from the `ATHERON_*` settings | | `Auth::handle(&RequestParts) -> Option` | The four routes under `/auth`, for any framework | | `Auth::require_user`, `Auth::current_user` | The signed-in person, or the refusal to answer with | | `CookieStore`, `MemoryStore` | Where the session lives; any `Store` of your own works | | `start_sign_in(client_id, redirect_uri, scopes) -> Started` | The authorize address and the verifier, state and nonce to keep | | `parse_callback(url, state, issuer) -> Result` | The code, after the state, issuer and error checks | | `Client::finish_sign_in`, `exchange_code`, `refresh_tokens`, `revoke_token`, `userinfo` | The token endpoint and userinfo | | `id_token_claims(token, client_id, nonce, &IdTokenOptions)` | An ID token's claims, checked for issuer, audience, expiry and nonce | | `verify_webhook_any`, `construct_event`, `WebhookSecrets` | Webhook signatures | | `follow_events(key, FollowOptions, handler) -> Result<(), Error>` | The events stream, with resume | | `Api::new(key)`, `app()`, `usage(days)` | The developer API | ## Sample apps One per framework, each `cargo run`: `samples/axum` and `samples/actix` in the SDK's repository. Their Atheron files are the examples on these pages. ## Frameworks [Axum](/docs/sdks/rust/axum) and [Actix Web](/docs/sdks/rust/actix). --- # Axum (Rust) URL: https://developers.aton-network.org/docs/sdks/rust/axum Sign in with Atheron in Axum. **Coming soon.** `aton-network` is not published on crates.io yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton-network-axum` adds the routes to an Axum router and gives you the `SignedIn` and `MaybeUser` extractors, a `require_user` middleware and a `Webhook` extractor. The extractors read the `Auth` from your router's state: an `Arc`, or any state with `FromRef` for one. Install with `cargo add aton-network aton-network-axum`; it ships on 30 October 2026. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET`, `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SESSION_SECRET` (32 random characters or more, which seal the session cookie) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Mount the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` The session is an AES-256-GCM sealed, HttpOnly, SameSite=Lax cookie that the browser can neither read nor change. To keep sessions on the server instead, set `Config::store` to `MemoryStore::new(true)` for one server, or to a `Store` of your own over Redis or a database. PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. --- # Actix Web (Rust) URL: https://developers.aton-network.org/docs/sdks/rust/actix Sign in with Atheron in Actix Web. **Coming soon.** `aton-network` is not published on crates.io yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton-network-actix` adds the routes to an Actix Web app as a scope and gives you the `SignedIn` and `MaybeUser` extractors, a `require_user` middleware and a `Webhook` extractor. They read the `Auth` from `app_data` as a `web::Data`. Install with `cargo add aton-network aton-network-actix`; it ships on 30 October 2026. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET`, `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SESSION_SECRET` (32 random characters or more, which seal the session cookie) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Mount the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php `). ## Webhooks **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` The session is an AES-256-GCM sealed, HttpOnly, SameSite=Lax cookie that the browser can neither read nor change. To keep sessions on the server instead, set `Config::store` to `MemoryStore::new(true)` for one server, or to a `Store` of your own over Redis or a database. PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. --- # Go SDK URL: https://developers.aton-network.org/docs/sdks/go Install and use github.com/Atheron-ATON-Network/atheron-sdk-go. **Coming soon.** `github.com/Atheron-ATON-Network/atheron-sdk-go` is not published on Go modules yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `atheron-sdk-go` is Sign in with Atheron for Go: PKCE, the authorize address, the code swap, userinfo, webhook signatures, the events stream and the developer API client, on the standard library alone. Gin and Echo integrations are modules of their own, so the core pulls in nothing. It ships on 30 October 2026. ## Install ```bash go get github.com/Atheron-ATON-Network/atheron-sdk-go # the core, with net/http go get github.com/Atheron-ATON-Network/atheron-sdk-go/gin # or /echo ``` Go 1.22 and later (Echo needs 1.23). ## Sign somebody in Build the address on your server and keep `started.Kept()` (the verifier, state and nonce) in their session. In a web app, the [net/http](/docs/sdks/go/net-http), [Gin](/docs/sdks/go/gin) or [Echo](/docs/sdks/go/echo) integration does this for you. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` `atheron.WebhookHandler` reads the raw body (up to 1 MiB), checks the signature against every secret you hold (two while you rotate), and answers 400 for a forgery, 500 when your function fails (so we deliver it again) and 204 otherwise. ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `FollowEvents` reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without `events:read`, or when your context ends. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `NewGraphQL` and `Query[T]` run typed queries: `atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, vars)`. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `Query` answers a `*GraphQLError` when any field failed, with the data that could be read on it; `Request` answers the data and the field errors side by side. Go has no WebSocket in its standard library, so use `FollowEvents` for events as they happen, or your own WebSocket client with `WSCloseAction`, `WSErrorAction` and `ResumeVariables`. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is an `*atheron.Error` with `Code` (for example `invalid_grant`), `Description`, the HTTP `Status` and, on a 429, `RetryAfter`. Find it in a chain with `atheron.AsError(err)`. See [Errors](/docs/errors) for every code. ## Reference | Item | What it does | |---|---| | `FromEnv(Config) (*Auth, error)` | Sign-in for a web app, from the `ATHERON_*` settings | | `(*Auth).Handler() http.Handler` | The four routes under `/auth` | | `(*Auth).RequireUser(http.Handler) http.Handler` | Somebody signed in, or off to sign in and back (401 for JSON) | | `UserFrom(ctx) User` | The person `RequireUser` let through | | `NewCookieStore`, `NewMemoryStore` | Where the session lives; any `Store` of your own works | | `StartSignIn(SignInOptions) (Started, error)` | The authorize address and the verifier, state and nonce to keep | | `ParseCallback(url, state, issuer) (string, error)` | The code, after the state, issuer and error checks | | `(*Client).FinishSignIn`, `ExchangeCode`, `RefreshTokens`, `RevokeToken`, `Userinfo` | The token endpoint and userinfo | | `IDTokenClaims(token, clientID, nonce, IDTokenOptions)` | An ID token's claims, checked for issuer, audience, expiry and nonce | | `VerifyWebhook`, `ConstructEvent`, `WebhookHandler` | Webhook signatures | | `FollowEvents(ctx, key, FollowOptions, func(Event) error) error` | The events stream, with resume | | `NewAPI(key) (*API, error)` | The developer API, generated from the OpenAPI description | ## Sample apps One per framework, each `go run .`: `samples/net-http`, `samples/gin` and `samples/echo` in the SDK's repository. Their Atheron files are the examples on these pages. ## Frameworks [net/http](/docs/sdks/go/net-http), [Gin](/docs/sdks/go/gin) and [Echo](/docs/sdks/go/echo). --- # net/http (Go) URL: https://developers.aton-network.org/docs/sdks/go/net-http Sign in with Atheron in net/http. **Coming soon.** `github.com/Atheron-ATON-Network/atheron-sdk-go` is not published on Go modules yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). The core package signs people in on net/http with nothing else installed: `auth.Handler()` for the routes, `auth.RequireUser` for the pages, and the session sealed in a cookie. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET`, `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SESSION_SECRET` (32 random characters or more, which seal the session cookie) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Mount the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` The session is an AES-GCM sealed, HttpOnly, SameSite=Lax cookie that the browser can neither read nor change. To keep sessions on the server instead, set `Config.Store` to `atheron.NewMemoryStore(true)` for one server, or to a `Store` of your own over Redis or a database. PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. --- # Gin (Go) URL: https://developers.aton-network.org/docs/sdks/go/gin Sign in with Atheron in Gin. **Coming soon.** `github.com/Atheron-ATON-Network/atheron-sdk-go` is not published on Go modules yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `atherongin` mounts the routes on a Gin group and gives you `RequireUser` and `CurrentUser`. Install with `go get github.com/Atheron-ATON-Network/atheron-sdk-go/gin`. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET`, `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SESSION_SECRET` (32 random characters or more, which seal the session cookie) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Mount the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. --- # Echo (Go) URL: https://developers.aton-network.org/docs/sdks/go/echo Sign in with Atheron in Echo. **Coming soon.** `github.com/Atheron-ATON-Network/atheron-sdk-go` is not published on Go modules yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `atheronecho` mounts the routes on an Echo group and gives you `RequireUser` and `CurrentUser`. Install with `go get github.com/Atheron-ATON-Network/atheron-sdk-go/echo`. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET`, `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) and `ATHERON_SESSION_SECRET` (32 random characters or more, which seal the session cookie) on your server. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. ## Mount the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. --- # Java and Kotlin SDK URL: https://developers.aton-network.org/docs/sdks/java Install and use org.aton-network:aton-network-sdk. **Coming soon.** `org.aton-network:aton-network-sdk` is not published on Maven Central yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton-network-sdk` is Sign in with Atheron for Java and Kotlin: PKCE, the authorize address, the code swap, userinfo, webhook signatures, the events stream and the developer API client, on the JDK alone (Java 17 and later). `aton-network-spring-boot-starter` adds Spring Boot and Spring Security on top. Both ship to Maven Central on 30 October 2026. ## Install ```bash # Gradle (Kotlin DSL) implementation("org.aton-network:aton-network-sdk:0.1.0") implementation("org.aton-network:aton-network-spring-boot-starter:0.1.0") # Spring Boot, with or without Spring Security ``` ```xml org.aton-network aton-network-spring-boot-starter 0.1.0 ``` ## Sign somebody in Build the address on your server and keep `started.kept()` (the verifier, state and nonce) in their session. In Spring Boot the [starter](/docs/sdks/java/spring-boot) does this for you. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `Events.follow` reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without `events:read`, or when your `stop` says so. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `GraphQL` runs queries on `java.net.http`: `query(GraphQL.USAGE_QUERY, Map.of("days", 30))`. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `query` throws `GraphQL.FieldErrors` when any field failed, with the data that could be read on it; `request` answers the data and the field errors side by side. Use `Events.follow` for events as they happen, or `java.net.http.WebSocket` with `wsCloseAction`, `wsErrorAction` and `resumeVariables`. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is an `AtheronException` (unchecked) with `code()` (for example `invalid_grant`), `description()`, the HTTP `status()` and, on a 429, `retryAfter()`. See [Errors](/docs/errors) for every code. ## Reference | Item | What it does | |---|---| | `Atheron.fromEnv()`, `Atheron.builder(clientId, redirectUri)` | Sign-in for a web app on any session (`Atheron.Session`) | | `begin(session, returnTo)`, `complete(session, callbackUrl)`, `user(session)`, `signOut(session)` | The flow, the person, signing out | | `Oidc.startSignIn`, `Oidc.parseCallback`, `Oidc.idTokenClaims`, `Oidc.safeReturnTo` | The pieces, for a framework of your own | | `AtheronClient`: `finishSignIn`, `exchangeCode`, `refreshTokens`, `revokeToken`, `userinfo` | The token endpoint and userinfo; the secret only ever as HTTP Basic | | `Webhooks.verify`, `Webhooks.constructEvent` | Webhook signatures | | `Events.follow(key, options, handler)` | The events stream, with resume | | `new AtheronApi(key)`: `app()`, `usage(days)` | The developer API, generated from the OpenAPI description | | `Transport.setStandard(...)` | Route every call through your own HTTP client, or a fake in tests | ## Sample apps `samples/spring-boot`, `samples/spring-security` and `samples/kotlin` in the SDK's repository, each `mvn spring-boot:run`. Their Atheron files are the examples on these pages. ## Frameworks [Spring Boot](/docs/sdks/java/spring-boot), [Spring Security](/docs/sdks/java/spring-security) and [Kotlin](/docs/sdks/java/kotlin). --- # Spring Boot (Java and Kotlin) URL: https://developers.aton-network.org/docs/sdks/java/spring-boot Sign in with Atheron in Spring Boot. **Coming soon.** `org.aton-network:aton-network-sdk` is not published on Maven Central yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). The starter signs people in on Spring Boot's servlet session with nothing to configure but your app's settings: the routes, `@AtheronRequired` for a page only for somebody signed in, `@AtheronUser` for the person, and `AtheronWebhooks` for deliveries. Spring Boot 3. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` on your server (or `atheron.client-id`, `atheron.client-secret` and `atheron.redirect-uri` in `application.properties`), and `ATHERON_WEBHOOK_SECRET` (or `atheron.webhook-secrets`) for webhooks. The starter adds `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session` on the servlet session. Register `https://your.app/auth/callback` as the redirect address. ## The app **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, the session ID changes when somebody signs in, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. With Spring Security on the classpath the routes stay off (Spring Security guards every path); use [Spring Security](/docs/sdks/java/spring-security), or set `atheron.routes.enabled=true` and permit them yourself. --- # Spring Security (Java and Kotlin) URL: https://developers.aton-network.org/docs/sdks/java/spring-security Sign in with Atheron in Spring Security. **Coming soon.** `org.aton-network:aton-network-sdk` is not published on Maven Central yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). With Spring Security, sign people in with `oauth2Login`. The starter registers Atheron as the `atheron` client (from `ATHERON_*` or `atheron.*`, unless you define registrations of your own) and `AtheronSecurity.oauth2Login` turns on PKCE, so Spring Security checks the state, the ID token's signature against our keys, its issuer, audience, expiry and nonce. Register `https://your.app/login/oauth2/code/atheron` as the redirect address and put it in `ATHERON_REDIRECT_URI`. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` on your server (or `atheron.client-id`, `atheron.client-secret` and `atheron.redirect-uri` in `application.properties`), and `ATHERON_WEBHOOK_SECRET` (or `atheron.webhook-secrets`) for webhooks. ## The security filter chain **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` `AtheronSecurity.revokeOnSignOut` revokes the refresh token at Atheron when somebody signs out. Sign-in starts at `/oauth2/authorization/atheron`. ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` Already have registrations for other providers? Add `AtheronSecurity.registration(clientId, clientSecret, redirectUri, scopes, null)` to your `ClientRegistrationRepository`. --- # Kotlin (Java and Kotlin) URL: https://developers.aton-network.org/docs/sdks/java/kotlin Sign in with Atheron in Kotlin. **Coming soon.** `org.aton-network:aton-network-sdk` is not published on Maven Central yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). Kotlin uses the same packages: `aton-network-sdk` for the core and `aton-network-spring-boot-starter` for Spring Boot. Every example on these pages has a Kotlin version; pick Kotlin at the top of any code block. For Spring, apply the `kotlin("plugin.spring")` Gradle plugin as Spring Boot's own Kotlin guide does. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` on your server (or `atheron.client-id`, `atheron.client-secret` and `atheron.redirect-uri` in `application.properties`), and `ATHERON_WEBHOOK_SECRET` (or `atheron.webhook-secrets`) for webhooks. ## The app **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, the session ID changes when somebody signs in, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. --- # C# and .NET SDK URL: https://developers.aton-network.org/docs/sdks/dotnet Install and use AtonNetwork.Sdk. **Coming soon.** `AtonNetwork.Sdk` is not published on NuGet yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `AtonNetwork.Sdk` is Sign in with Atheron for .NET: PKCE, the authorize address, the code swap, userinfo, webhook signatures, the events stream and the developer API client, on .NET 8 alone. `AtonNetwork.AspNetCore` adds ASP.NET Core. Both ship to NuGet on 30 October 2026. ## Install ```bash dotnet add package AtonNetwork.Sdk # the core dotnet add package AtonNetwork.AspNetCore # ASP.NET Core ``` ## Sign somebody in Build the address on your server and keep `started.Kept` (the verifier, state and nonce) in their session. In ASP.NET Core, [the integration](/docs/sdks/dotnet/aspnet-core) does this for you. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `AtheronEvents.Follow` is an `IAsyncEnumerable`: it reconnects when the connection drops and resumes after the last event it handed you; it stops on a refusal that waiting cannot fix, such as a key without `events:read`, when you break out of the loop, or when your token is cancelled. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `AtheronGraphQL` runs typed queries: `QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 })`. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `QueryAsync` throws an `AtheronGraphQLException` when any field failed, with the data that could be read on it; `RequestAsync` answers the data and the field errors side by side. Use `AtheronEvents.Follow` for events as they happen, or `ClientWebSocket` with `WsCloseAction`, `WsErrorAction` and `ResumeVariables`. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is an `AtheronException` with `Code` (for example `invalid_grant`), `Description`, the HTTP `Status` and, on a 429, `RetryAfter`. See [Errors](/docs/errors) for every code. ## Reference | Item | What it does | |---|---| | `Oidc.StartSignIn`, `Oidc.ParseCallback`, `Oidc.IdTokenClaims`, `Oidc.SafeReturnTo` | The sign-in pieces | | `AtheronClient`: `FinishSignInAsync`, `ExchangeCodeAsync`, `RefreshTokensAsync`, `RevokeTokenAsync`, `UserinfoAsync` | The token endpoint and userinfo; the secret only ever as HTTP Basic | | `Webhooks.Verify`, `Webhooks.ConstructEvent` | Webhook signatures | | `AtheronEvents.Follow(key, options, cancel)` | The events stream, with resume | | `new AtheronApi(key)`: `AppAsync()`, `UsageAsync(days)` | The developer API, generated from the OpenAPI description | | `AtheronHttp.Client` | Route every call through your own HttpClient, or a fake in tests | ## Sample app `samples/aspnet-core` in the SDK's repository, `dotnet run`. Its Atheron files are the examples on these pages. --- # ASP.NET Core (C# and .NET) URL: https://developers.aton-network.org/docs/sdks/dotnet/aspnet-core Sign in with Atheron in ASP.NET Core. **Coming soon.** `AtonNetwork.Sdk` is not published on NuGet yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `AtonNetwork.AspNetCore` signs people in on ASP.NET Core's cookie authentication: `AddAtheron()` and `MapAtheron()` give you the routes, `RequireAuthorization()` works as it always does, and `AtheronWebhooks` checks deliveries from the raw body. .NET 8 and later. Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` (or `Atheron:ClientId`, `Atheron:ClientSecret` and `Atheron:RedirectUri` in appsettings.json), and `ATHERON_WEBHOOK_SECRET` for webhooks. The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. Register `https://your.app/auth/callback` as the redirect address. ## The app **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` Or in one line: `app.MapAtheronWebhook("/webhooks/atheron", (e, http) => ...)`, which answers 400 for a forgery, 500 when your handler throws (so we deliver it again) and 204 otherwise. PKCE, `state` and `nonce` are checked on every sign-in, the flow is sealed with your Data Protection keys, spent on use and lasts ten minutes, a return address is always a path on your site, signing out revokes the refresh token at Atheron, and a sign-out from another site is refused. Across several servers, share the Data Protection keys as ASP.NET Core's own guide describes. --- # PHP SDK URL: https://developers.aton-network.org/docs/sdks/php Install and use aton-network/sdk. **Coming soon.** `aton-network/sdk` is not published on Packagist yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton-network/sdk` is Sign in with Atheron for PHP: PKCE, the authorize address, the code swap, userinfo, webhook signatures, the events stream and the developer API client, on PHP 8.1 and its own extensions alone. A Laravel Socialite driver, a Symfony bundle and a WordPress plugin come with it. ## Install ```bash composer require aton-network/sdk ``` PHP 8.1 and later, with `ext-openssl` to reach Atheron over https (it is on almost everywhere). No HTTP library is needed: the SDK uses PHP's own streams, follows no redirects and gives every call ten seconds. To route its calls through your own client or a proxy, hand `AtonNetwork\Transport::use()` a function. ## Sign somebody in Build the address on your server and keep `$started->kept()` (the verifier, state and nonce) in their session. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` For a whole web app on PHP's own session, `AtonNetwork\Atheron` does both ends and keeps the person: `begin()` in your sign-in route, `complete()` in your callback, `user()` anywhere after and `signOut()` on a POST from your own site. The SDK's plain PHP sample app (`samples/php`) is one file. ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` Pass the raw body, exactly as it arrived: `file_get_contents('php://input')`, or `$request->getContent()` in Laravel and Symfony. ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `Events::follow` is a generator. It reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without `events:read`. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `AtonNetwork\GraphQL` runs queries on PHP's own streams: `query(GraphQL::USAGE_QUERY, ['days' => 30])`. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `query()` throws `GraphQLFieldErrors` when any field failed, with the data that could be read on it; `request()` answers the data and the field errors side by side. Use `Events::follow` for events as they happen, or a WebSocket library of your own with `wsCloseAction`, `wsErrorAction` and `resumeVariables`. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is thrown as `AtonNetwork\AtheronError` with `$e->error` (for example `invalid_grant`), `$e->description`, the HTTP `$e->status` and, on a 429, `$e->retryAfter`. See [Errors](/docs/errors) for every code. ## Frameworks [Laravel Socialite](/docs/sdks/php/laravel), [Symfony](/docs/sdks/php/symfony) and the [WordPress plugin](/docs/sdks/php/wordpress). --- # Laravel Socialite (PHP) URL: https://developers.aton-network.org/docs/sdks/php/laravel Sign in with Atheron in Laravel Socialite. **Coming soon.** `aton-network/sdk` is not published on Packagist yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `aton-network/sdk` adds an `atheron` driver to Laravel Socialite. Laravel finds it on its own once the package is installed. ## Install ```bash composer require aton-network/sdk laravel/socialite ``` Add the app to `config/services.php`, with the values in `.env`: ```php 'atheron' => [ 'client_id' => env('ATHERON_CLIENT_ID'), 'client_secret' => env('ATHERON_CLIENT_SECRET'), 'redirect' => env('ATHERON_REDIRECT_URI'), 'webhook_secret' => env('ATHERON_WEBHOOK_SECRET'), ], ``` Register `https://your.app/auth/callback` as the redirect address, and give your users table a nullable, unique `atheron_id` column: people are found by the ID your app was given for them (`sub`, made for your app alone), never by email. ## Sign in **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` Ask for more with `Socialite::driver('atheron')->scopes(['membership'])->redirect()`. `Auth::login()` gives the person a new session ID. ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in. The callback is checked by the same rules as every Atheron SDK: the state first, then the issuer, then an error Atheron sent, then the code. `stateless()` is refused. - An ID token that comes back is held to its issuer, audience, expiry and nonce. - The secret goes in the Authorization header, never in the form. - An email Atheron has not verified is not handed on as the person's: `getEmail()` is `null` for it. --- # Symfony (PHP) URL: https://developers.aton-network.org/docs/sdks/php/symfony Sign in with Atheron in Symfony. **Coming soon.** `aton-network/sdk` is not published on Packagist yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `AtonNetwork\Symfony\AtheronBundle` signs people in on Symfony's session: the routes, the person on every request and paths that need somebody signed in. ## Install ```bash composer require aton-network/sdk ``` Add `AtonNetwork\Symfony\AtheronBundle::class => ['all' => true]` to `config/bundles.php` and import its routes in `config/routes/atheron.yaml`: ```yaml atheron: resource: '@AtheronBundle/config/routes.php' prefix: /auth ``` ## Configure it **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. Link to `/auth/sign-in?return_to=/account` to sign somebody in and bring them back. Register `https://your.app/auth/callback` as the redirect address. ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, the session ID changes at sign-in, a return address is always a path on your site, and signing out revokes the refresh token at Atheron. --- # WordPress plugin (PHP) URL: https://developers.aton-network.org/docs/sdks/php/wordpress Sign in with Atheron in WordPress plugin. **Coming soon.** `aton-network/sdk` is not published on Packagist yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). **Atheron Sign In** puts a "Sign in with Atheron" button on your WordPress login page. It carries its own copy of the SDK, so there is nothing to install with Composer. ## Install Install the plugin from the WordPress plugin directory (search for Atheron Sign In) and activate it. Settings > Atheron shows the two addresses to register for your app: the redirect address, `https://your.site/wp-login.php?action=atheron_callback`, and the webhook address, `https://your.site/wp-json/atheron/v1/webhook`. ## Set it up **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` Constants in wp-config.php win over the settings page and keep the secrets out of the database. On the settings page you choose whether somebody new gets a WordPress account, and whether an existing account with the same verified email is linked. ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` The plugin checks the signature, then hands the event to the `atheron_webhook_event` action. `atheron_user_by_sub()` finds the WordPress user an event is about. ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in. WordPress has no session, so the flow waits in a transient for ten minutes, keyed by a random value in an HTTP-only cookie on the browser that started it, and is spent on use. - People are linked by the ID your site was given for them (user meta `atheron_sub`), never by email alone unless you turn that on, and an address already on the site never gets a second account. - The plugin keeps no tokens: it signs people in and keeps only the ID your site was given for them. - A return address is always a page on your own site. --- # Ruby SDK URL: https://developers.aton-network.org/docs/sdks/ruby Install and use aton-network. **Coming soon.** `aton-network` is not published on RubyGems yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `atheron` is Sign in with Atheron for Ruby: PKCE, the authorize address, the code swap, userinfo, webhook signatures, the events stream and the developer API client, on Ruby 3.1 and its standard library alone. A Rails engine and an OmniAuth strategy come with it. ## Install ```bash bundle add aton-network # or: gem install aton-network ``` The SDK uses Net::HTTP, follows no redirects and gives every call ten seconds. To route its calls through your own client or a proxy, set `AtonNetwork.transport` to anything that answers `call(method, url, headers, body)`. ## Sign somebody in Build the address on your server and keep `started.kept` (the verifier, state and nonce) in their session. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` For a whole web app on Rack's session, `AtonNetwork::Auth` does both ends and keeps the person: `begin` in your sign-in route, `complete` in your callback, `user` anywhere after and `sign_out` on a POST from your own site. The SDK's Rack sample app (`samples/rack`) is one file. ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` Pass the raw body, exactly as it arrived: `request.raw_post` in Rails, `request.body.read` in Rack. ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `AtonNetwork::Events.follow` reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without `events:read`. Without a block it answers an Enumerator. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `AtonNetwork::GraphQL` runs queries on the standard library: `query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })`. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `query` raises `AtonNetwork::GraphQL::FieldErrors` when any field failed, with the data that could be read on it; `request` answers the data and the field errors side by side. Use `AtonNetwork::Events.follow` for events as they happen, or a WebSocket gem of your own with `ws_close_action`, `ws_error_action` and `resume_variables`. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is raised as `AtonNetwork::Error` with `code` (for example `invalid_grant`), `description`, the HTTP `status` and, on a 429, `retry_after`. See [Errors](/docs/errors) for every code. ## Frameworks [Rails](/docs/sdks/ruby/rails) and the [OmniAuth strategy](/docs/sdks/ruby/omniauth). --- # Rails (Ruby) URL: https://developers.aton-network.org/docs/sdks/ruby/rails Sign in with Atheron in Rails. **Coming soon.** `aton-network` is not published on RubyGems yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `AtonNetwork::Engine` signs people in on Rails' own session: the routes, `current_atheron_user` and `require_atheron_user` for your controllers. Bundler loads it with the gem. ## Install ```bash bundle add aton-network ``` Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) on your server, or set `AtonNetwork::Engine.auth = AtonNetwork::Auth.new(...)` in an initializer. ## Mount the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` The engine answers `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site) and `/auth/session`. Link to `/auth/sign-in?return_to=/account` to sign somebody in and bring them back. If you mount it somewhere else, set `AtonNetwork::Engine.mount_path` to match. ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, the session is a new one at sign-in, a return address is always a path on your site, and signing out revokes the refresh token at Atheron. Sign-out is protected by Rails' forgery protection as well as the same-origin check. --- # OmniAuth strategy (Ruby) URL: https://developers.aton-network.org/docs/sdks/ruby/omniauth Sign in with Atheron in OmniAuth strategy. **Coming soon.** `aton-network` is not published on RubyGems yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `omniauth/strategies/atheron` is Atheron as an OmniAuth strategy, for Rails (with Devise or on its own), Sinatra or any Rack app. It comes with the `atheron` gem. ## Install ```bash bundle add aton-network omniauth ``` In Rails, add `omniauth-rails_csrf_protection` too: OmniAuth starts a sign-in only from a POST that carries the page's authenticity token. ## Add the provider **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` Register `https://your.app/auth/atheron/callback` as the redirect address. `env["omniauth.auth"]` then holds `uid` (the ID your app was given for the person, made for your app alone: key your records on it, never on email), `info` (name, email, nickname, image), `credentials` (the tokens) and `extra.raw_info` (userinfo's claims). ## How it keeps you safe - PKCE (S256), `state` and `nonce` on every sign-in. The callback is checked by the same rules as every Atheron SDK: the state first, then the issuer, then an error Atheron sent, then the code. A failure goes to OmniAuth's failure route with its code, such as `invalid_state`. - An ID token that comes back is held to its issuer, audience, expiry and nonce. - The redirect address sent with the code is the registered one, never with the callback's own query. - An email Atheron has not verified is not handed on: `info.email` is absent for it. --- # Elixir SDK URL: https://developers.aton-network.org/docs/sdks/elixir Install and use aton_network. **Coming soon.** `aton_network` is not published on Hex yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `atheron` is Sign in with Atheron for Elixir: PKCE, the authorize address, the code swap, userinfo, webhook signatures, the events stream and the developer API client, on Elixir 1.18 and Erlang/OTP's own `:httpc`. Plug and Phoenix get routes and plugs. ## Install ```elixir def deps do [{:aton_network, "~> 0.1"}] end ``` Every call checks TLS against the system's certificates, follows no redirects and has ten seconds. To route the SDK through your own HTTP client or a proxy, set `config :aton_network, transport: MyTransport` (a module with `call/4`, or a function of four arguments). Functions answer `{:ok, value}` or `{:error, %AtonNetwork.Error{}}`, so they sit in a `with`. ## Sign somebody in Build the address on your server and keep `AtonNetwork.Started.kept(started)` (the verifier, state and nonce) in their session. **cURL** ```bash # The authorize request is a page the person opens, not an API call. Build the address on your # server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it. VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n') CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" ``` **TypeScript** ```ts // Where to send somebody to sign in. Keep verifier, state and nonce for this browser (an HTTP-only // cookie), and redirect them to url. import { startSignIn, type Started } from "@aton-network/sdk"; export async function beginSignIn(clientId: string, redirectUri: string): Promise { return startSignIn({ clientId, redirectUri, scopes: ["profile", "email"] }); } ``` **Python** ```python # Where to send somebody to sign in. Keep verifier, state and nonce in their session until they come back. from aton_network import Started, start_sign_in def begin_sign_in(client_id: str, redirect_uri: str) -> Started: return start_sign_in(client_id=client_id, redirect_uri=redirect_uri, scopes=["profile", "email"]) ``` **Rust** ```rust // Where to send somebody to sign in. Keep started.kept() (the verifier, state and nonce) in their // session until they come back, and redirect them to started.url. use aton_network::{start_sign_in, Started}; pub fn begin_sign_in(client_id: &str, redirect_uri: &str) -> Started { start_sign_in(client_id, redirect_uri, &["profile", "email"]) } ``` **Go** ```go // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package authorizeurl import "github.com/Atheron-ATON-Network/atheron-sdk-go" func BeginSignIn(clientID, redirectURI string) (atheron.Started, error) { return atheron.StartSignIn(atheron.SignInOptions{ ClientID: clientID, RedirectURI: redirectURI, Scopes: []string{"profile", "email"}, }) } ``` **Java** ```java // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.java; import java.util.List; import org.atonnetwork.atheron.Oidc; public class BeginSignIn { public static Oidc.Started beginSignIn(String clientId, String redirectUri) { return Oidc.startSignIn(clientId, redirectUri, List.of("profile", "email")); } } ``` **Kotlin** ```kotlin // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. package examples.authorizeurl.kotlin import org.atonnetwork.atheron.Oidc fun beginSignIn(clientId: String, redirectUri: String): Oidc.Started = Oidc.startSignIn(clientId, redirectUri, listOf("profile", "email")) ``` **C#** ```csharp // Where to send somebody to sign in. Keep the verifier, state and nonce in their session until they come back. using AtonNetwork; namespace Examples.AuthorizeUrl; public static class BeginSignIn { public static Started Begin(string clientId, string redirectUri) => Oidc.StartSignIn(clientId, redirectUri, ["profile", "email"]); } ``` **PHP** ```php { return finishSignIn({ callbackUrl, kept, clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET!, redirectUri: "https://app.example.com/callback", }); } ``` **Python** ```python # In the route your redirect address points at: check state, then swap the code for tokens. import os from aton_network import Client def complete_sign_in(callback_url: str, kept: dict) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.finish_sign_in(callback_url=callback_url, kept=kept, redirect_uri="https://app.example.com/callback") ``` **Rust** ```rust // In the route your redirect address points at: check state, then swap the code for tokens. use aton_network::{Client, Error, Kept, Tokens}; pub async fn complete_sign_in(callback_url: &str, kept: &Kept) -> Result { let client = Client::from_env(); // ATHERON_CLIENT_ID and ATHERON_CLIENT_SECRET client.finish_sign_in(callback_url, kept, "https://app.example.com/callback").await } ``` **Go** ```go // In the route your redirect address points at: check state, then swap the code for tokens. package tokenexchange import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CompleteSignIn(ctx context.Context, callbackURL string, kept atheron.Kept) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.FinishSignIn(ctx, callbackURL, kept, "https://app.example.com/callback") } ``` **Java** ```java // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.java; import org.atonnetwork.atheron.AtheronClient; import org.atonnetwork.atheron.Oidc; public class CompleteSignIn { public static AtheronClient.Tokens completeSignIn(String callbackUrl, Oidc.Kept kept) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **Kotlin** ```kotlin // In the route your redirect address points at: check state, then swap the code for tokens. package examples.tokenexchange.kotlin import org.atonnetwork.atheron.AtheronClient import org.atonnetwork.atheron.Oidc fun completeSignIn(callbackUrl: String, kept: Oidc.Kept): AtheronClient.Tokens { val client = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")) return client.finishSignIn(callbackUrl, kept, "https://app.example.com/callback") } ``` **C#** ```csharp // In the route your redirect address points at: check state, then swap the code for tokens. using AtonNetwork; namespace Examples.TokenExchange; public static class CompleteSignIn { public static Task RunAsync(string callbackUrl, Kept kept) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.FinishSignInAsync(callbackUrl, kept, "https://app.example.com/callback"); } } ``` **PHP** ```php finishSignIn($callbackUrl, $kept, 'https://app.example.com/callback'); } ``` **Ruby** ```ruby # In the route your redirect address points at: check state, then swap the code for tokens. require "aton_network" def complete_sign_in(callback_url, kept) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.finish_sign_in(callback_url: callback_url, kept: kept, redirect_uri: "https://app.example.com/callback") end ``` **Elixir** ```elixir # In the route your redirect address points at: check state, then swap the code for tokens. defmodule MyApp.Callback do def complete_sign_in(callback_url, kept) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.finish_sign_in(callback_url, kept, "https://app.example.com/callback") end end ``` For a whole web app, `AtonNetwork.Auth` does both ends: `begin/2` gives the flow to keep in the session and the address to send the browser to, and `complete/3` checks the flow you took back out and answers the person and what to keep. [Phoenix](/docs/sdks/elixir/phoenix) does the keeping for you. ## Read who it is now **cURL** ```bash # Who the person is as of now, with an access token issued to your client. curl https://accounts.aton-network.org/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` **TypeScript** ```ts // The claims their consent allows, read fresh. Key your records on sub, never on email. import { userinfo } from "@aton-network/sdk"; export async function whoIsThis(accessToken: string): Promise<{ sub: string; email?: unknown }> { const me = await userinfo(accessToken); return { sub: me.sub, email: me.email }; } ``` **Python** ```python # The person as of now. Call it after signing in, and again when you need fresh details. from aton_network import userinfo def who_is_this(access_token: str) -> dict: return userinfo(access_token) ``` **Rust** ```rust // Who the access token belongs to, as of now. use aton_network::{userinfo, Error, User}; pub async fn who_is_this(access_token: &str) -> Result { userinfo(access_token).await } ``` **Go** ```go // Who the access token belongs to, as of now. package userinfo import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func WhoIsThis(ctx context.Context, accessToken string) (atheron.User, error) { return atheron.Userinfo(ctx, accessToken, "", nil) } ``` **Java** ```java // Who the access token belongs to, as of now. package examples.userinfo.java; import java.util.Map; import org.atonnetwork.atheron.AtheronClient; public class WhoIsThis { public static Map whoIsThis(String accessToken) { return new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken); } } ``` **Kotlin** ```kotlin // Who the access token belongs to, as of now. package examples.userinfo.kotlin import org.atonnetwork.atheron.AtheronClient fun whoIsThis(accessToken: String): Map = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), null).userinfo(accessToken) ``` **C#** ```csharp // Who the access token belongs to, as of now. using System.Text.Json; using AtonNetwork; namespace Examples.Userinfo; public static class WhoIsThis { public static Task RunAsync(string accessToken) => Oidc.UserinfoAsync(accessToken); } ``` **PHP** ```php $me['sub'], 'email' => $me['email'] ?? null]; } ``` **Ruby** ```ruby # The claims their consent allows, read fresh. Key your records on sub, never on email. require "aton_network" def who_is_this(access_token) me = AtonNetwork::OIDC.userinfo(access_token) { "sub" => me["sub"], "email" => me["email"] } end ``` **Elixir** ```elixir # The claims their consent allows, read fresh. Key your records on sub, never on email. defmodule MyApp.People do def who_is_this(access_token) do with {:ok, me} <- AtonNetwork.OIDC.userinfo(access_token) do {:ok, %{"sub" => me["sub"], "email" => me["email"]}} end end end ``` ## Keep them signed in **cURL** ```bash # A new access token and a new refresh token for the newest refresh token you hold. The old one stops working. curl https://accounts.aton-network.org/oauth/token \ -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` **TypeScript** ```ts // Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. import { AtheronError, ISSUER, type Tokens } from "@aton-network/sdk"; export async function refreshTokens(refreshToken: string): Promise { const basic = btoa(`${process.env.ATHERON_CLIENT_ID}:${process.env.ATHERON_CLIENT_SECRET}`); const answer = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), }); const json = (await answer.json()) as Tokens & { error?: string; error_description?: string }; if (!answer.ok) throw new AtheronError(json.error ?? "server_error", json.error_description ?? "", answer.status); return json; } ``` **Python** ```python # New tokens from a refresh token. Keep the new refresh token the answer carries: the old one stops working. import os from aton_network import Client def refresh_tokens(refresh_token: str) -> dict: client = Client(client_id=os.environ["ATHERON_CLIENT_ID"], client_secret=os.environ["ATHERON_CLIENT_SECRET"]) return client.refresh_tokens(refresh_token) ``` **Rust** ```rust // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. use aton_network::{Client, Error, Tokens}; pub async fn refresh_tokens(refresh_token: &str) -> Result { Client::from_env().refresh_tokens(refresh_token).await } ``` **Go** ```go // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package refresh import ( "context" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RefreshTokens(ctx context.Context, refreshToken string) (*atheron.Tokens, error) { client := &atheron.Client{ClientID: os.Getenv("ATHERON_CLIENT_ID"), ClientSecret: os.Getenv("ATHERON_CLIENT_SECRET")} return client.RefreshTokens(ctx, refreshToken) } ``` **Java** ```java // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.java; import org.atonnetwork.atheron.AtheronClient; public class RefreshTokens { public static AtheronClient.Tokens refreshTokens(String refreshToken) { AtheronClient client = new AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")); return client.refreshTokens(refreshToken); } } ``` **Kotlin** ```kotlin // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. package examples.refresh.kotlin import org.atonnetwork.atheron.AtheronClient fun refreshTokens(refreshToken: String): AtheronClient.Tokens = AtheronClient(System.getenv("ATHERON_CLIENT_ID"), System.getenv("ATHERON_CLIENT_SECRET")).refreshTokens(refreshToken) ``` **C#** ```csharp // New tokens from a refresh token (asked for with offline_access). Keep the new refresh token. using AtonNetwork; namespace Examples.Refresh; public static class RefreshTokens { public static Task RunAsync(string refreshToken) { var client = new AtheronClient(Environment.GetEnvironmentVariable("ATHERON_CLIENT_ID")!, Environment.GetEnvironmentVariable("ATHERON_CLIENT_SECRET")); return client.RefreshTokensAsync(refreshToken); } } ``` **PHP** ```php refreshTokens($refreshToken); } ``` **Ruby** ```ruby # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. require "aton_network" def refresh_tokens(refresh_token) client = AtonNetwork::Client.new(client_id: ENV.fetch("ATHERON_CLIENT_ID"), client_secret: ENV.fetch("ATHERON_CLIENT_SECRET")) client.refresh_tokens(refresh_token) end ``` **Elixir** ```elixir # Refresh tokens rotate: store the new one before you use the access token, and send one refresh at a time. defmodule MyApp.Refresh do def refresh_tokens(refresh_token) do [client_id: System.fetch_env!("ATHERON_CLIENT_ID"), client_secret: System.fetch_env!("ATHERON_CLIENT_SECRET")] |> AtonNetwork.Client.new() |> AtonNetwork.Client.refresh_tokens(refresh_token) end end ``` ## Check a webhook **TypeScript** ```ts // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk"; export async function handleWebhook(request: Request): Promise { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` Pass the raw body, exactly as it arrived. In Phoenix, `AtonNetwork.Plug.RawBody` keeps it for you. ## Follow the events stream **cURL** ```bash # Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume. curl -N https://developers.aton-network.org/api/v1/events \ -H "Authorization: Bearer $ATHERON_API_KEY" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: $LAST_EVENT_ID" ``` **TypeScript** ```ts // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. import { followEvents } from "@aton-network/sdk"; export async function followAppEvents(apiKey: string, lastId: string | null, handle: (type: string, sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of followEvents({ apiKey, lastEventId: lastId, types: ["user.signed_in", "consent.revoked"], ...(signal ? { signal } : {}) })) { handle(event.type, event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. from aton_network import follow_events def follow_app_events(api_key: str, last_id, handle, stop=None): for event in follow_events(api_key, last_event_id=last_id, types=["user.signed_in", "consent.revoked"], stop=stop): handle(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. use aton_network::{follow_events, Error, Event, FollowOptions, Next}; pub async fn follow_app_events(api_key: &str, last_id: Option, mut handle: impl FnMut(&str, Option<&str>)) -> Result<(), Error> { let options = FollowOptions { last_event_id: last_id, // the last id you kept, after a restart types: vec!["user.signed_in".into(), "consent.revoked".into()], ..Default::default() }; follow_events(api_key, options, |event: Event| { handle(event.kind(), event.sub()); // Keep event.id somewhere durable, to pass back as last_id after a restart. async { Ok(Next::Continue) } }) .await } ``` **Go** ```go // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package apievents import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func FollowAppEvents(ctx context.Context, apiKey, lastID string, handle func(eventType, sub string)) (string, error) { err := atheron.FollowEvents(ctx, apiKey, atheron.FollowOptions{ LastEventID: lastID, Types: []string{"user.signed_in", "consent.revoked"}, }, func(event atheron.Event) error { handle(event.Type, event.Sub()) lastID = event.ID // keep it somewhere durable, to pass back after a restart return nil }) return lastID, err } ``` **Java** ```java // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.java; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.atonnetwork.atheron.Events; public class FollowAppEvents { public static String followAppEvents(String apiKey, String lastId, BiConsumer handle, BooleanSupplier stop) throws InterruptedException { String[] last = {lastId}; Events.follow(apiKey, new Events.Options().lastEventId(lastId).types(List.of("user.signed_in", "consent.revoked")).stop(stop), event -> { handle.accept(event.type(), event.sub()); last[0] = event.id(); // keep it somewhere durable, to pass back after a restart }); return last[0]; } } ``` **Kotlin** ```kotlin // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. package examples.apievents.kotlin import org.atonnetwork.atheron.Events fun followAppEvents(apiKey: String, lastId: String?, handle: (type: String, sub: String?) -> Unit, stop: () -> Boolean): String? { var last = lastId Events.follow(apiKey, Events.Options().lastEventId(lastId).types(listOf("user.signed_in", "consent.revoked")).stop { stop() }) { event -> handle(event.type(), event.sub()) last = event.id() // keep it somewhere durable, to pass back after a restart } return last } ``` **C#** ```csharp // Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. using AtonNetwork; namespace Examples.ApiEvents; public static class FollowAppEvents { public static async Task RunAsync(string apiKey, string? lastId, Action handle, CancellationToken cancel) { var options = new FollowOptions { LastEventId = lastId, Types = ["user.signed_in", "consent.revoked"] }; await foreach (var e in AtheronEvents.Follow(apiKey, options, cancel)) { handle(e.Type, e.Sub); lastId = e.Id; // keep it somewhere durable, to pass back after a restart } return lastId; } } ``` **PHP** ```php type, $event->sub()); $lastId = $event->id; // keep it somewhere durable, to pass back after a restart } return $lastId; } ``` **Ruby** ```ruby # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. require "aton_network" def follow_app_events(api_key, last_id, stop: nil, &handle) AtonNetwork::Events.follow(api_key, types: %w[user.signed_in consent.revoked], last_event_id: last_id, stop: stop) do |event| handle.call(event.type, event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart end last_id end ``` **Elixir** ```elixir # Follow your app's events from a server: reconnects on its own and resumes after the last id you handled. defmodule MyApp.AppEvents do def follow_app_events(api_key, last_id, handle, stop \\ fn -> false end) do AtonNetwork.Events.follow(api_key, [types: ["user.signed_in", "consent.revoked"], last_event_id: last_id, stop: stop], fn event -> handle.(event.type, AtonNetwork.Event.sub(event)) # The answer, {:ok, last_id}, is the id to keep somewhere durable and pass back after a restart. end) end end ``` `AtonNetwork.Events.follow/3` reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without `events:read`, or when `:stop` says so. Run it in a Task or a GenServer of your own. See the [events stream](/docs/reference/api-events). ## GraphQL **TypeScript** ```ts // Your app and its usage in one GraphQL request, then its events as they happen over the subscription. // Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package. import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk"; export async function usageThisMonth(apiKey: string): Promise { const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 }); return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0; } export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise { for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) { forget(event.data.sub); lastId = event.id; // keep it somewhere durable, to pass back after a restart } return lastId; } ``` **Python** ```python # Your app and its usage in one GraphQL request, then its events as they happen over the subscription. # Subscriptions need the graphql extra: pip install "aton-network[graphql]". from aton_network import USAGE_QUERY, AtheronGraphQL def usage_this_month(api_key: str): usage = AtheronGraphQL(api_key).query(USAGE_QUERY, {"days": 30})["usage"] return sum(day["calls"] for day in usage["usage"]) if usage else 0 def watch_consent(api_key: str, last_id, forget, stop=None): for event in AtheronGraphQL(api_key).events(types=["consent.revoked"], last_event_id=last_id, stop=stop): forget(event.sub) last_id = event.id # keep it somewhere durable, to pass back after a restart return last_id ``` **Rust** ```rust // How many calls your app made this month, in one typed GraphQL request. use aton_network::{Error, GraphQl, UsageQueryData, USAGE_QUERY}; pub async fn calls_this_month(api_key: &str) -> Result { let data: UsageQueryData = GraphQl::new(api_key)?.query(&USAGE_QUERY, Some(serde_json::json!({ "days": 30 }))).await?; Ok(data.usage.map(|u| u.usage.iter().map(|day| day.calls).sum()).unwrap_or(0)) } ``` **Go** ```go // How many calls your app made this month, in one typed GraphQL request. package apigraphql import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func CallsThisMonth(ctx context.Context, apiKey string) (int, error) { gql, err := atheron.NewGraphQL(apiKey) if err != nil { return 0, err } got, err := atheron.Query[atheron.UsageQueryData](ctx, gql, atheron.UsageQuery, map[string]any{"days": 30}) if err != nil || got.Usage == nil { return 0, err } calls := 0 for _, day := range got.Usage.Usage { calls += day.Calls } return calls, nil } ``` **Java** ```java // How many calls your app made this month, in one GraphQL request. package examples.apigraphql.java; import java.util.List; import java.util.Map; import org.atonnetwork.atheron.GraphQL; public class CallsThisMonth { @SuppressWarnings("unchecked") public static long callsThisMonth(String apiKey) { Map usage = (Map) new GraphQL(apiKey).query(GraphQL.USAGE_QUERY, Map.of("days", 30)).get("usage"); if (usage == null) return 0; return ((List>) usage.get("usage")).stream().mapToLong(day -> ((Number) day.get("calls")).longValue()).sum(); } } ``` **C#** ```csharp // How many calls your app made this month, in one typed GraphQL request. using AtonNetwork; namespace Examples.ApiGraphql; public static class CallsThisMonth { public static async Task RunAsync(string apiKey) { var data = await new AtheronGraphQL(apiKey).QueryAsync(AtheronGraphQL.UsageQuery, new { days = 30 }); return data.Usage?.Usage.Sum(day => day.Calls) ?? 0; } } ``` **PHP** ```php query(GraphQL::USAGE_QUERY, ['days' => 30])['usage']; return $usage === null ? 0 : array_sum(array_column($usage['usage'], 'calls')); } ``` **Ruby** ```ruby # How many calls your app made this month, in one GraphQL request. require "aton_network" def calls_this_month(api_key) usage = AtonNetwork::GraphQL.new(api_key).query(AtonNetwork::GraphQL::USAGE_QUERY, { "days" => 30 })["usage"] usage ? usage["usage"].sum { |day| day["calls"] } : 0 end ``` **Elixir** ```elixir # How many calls your app made this month, in one GraphQL request. defmodule MyApp.Calls do alias AtonNetwork.GraphQL def calls_this_month(api_key) do case api_key |> GraphQL.new() |> GraphQL.query(GraphQL.usage_query(), %{"days" => 30}) do {:ok, %{"usage" => nil}} -> {:ok, 0} {:ok, %{"usage" => %{"usage" => days}}} -> {:ok, days |> Enum.map(& &1["calls"]) |> Enum.sum()} {:error, error} -> {:error, error} end end end ``` `AtonNetwork.GraphQL` runs queries on `:httpc`: `query(graphql, AtonNetwork.GraphQL.usage_query(), %{"days" => 30})`. The documents the SDK ships (the App, Usage, Overview and Events documents the [GraphQL reference](/docs/reference/graphql) publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. `query` answers `{:error, {:field_errors, data, errors}}` when any field failed, with the data that could be read; `request` answers `{:ok, data, field_errors}`. Use `AtonNetwork.Events.follow/3` for events as they happen, or a WebSocket client of your own with `ws_close_action/1`, `ws_error_action/1` and `resume_variables/2`. ## The developer API **cURL** ```bash # Your app as registered, for the environment this API key belongs to. Needs app:read. curl https://developers.aton-network.org/api/v1/app \ -H "Authorization: Bearer $ATHERON_API_KEY" ``` **TypeScript** ```ts // In a deploy check: confirm the redirect addresses your code expects are the ones registered. export async function registeredRedirects(apiKey: string): Promise { const answer = await fetch("https://developers.aton-network.org/api/v1/app", { headers: { authorization: `Bearer ${apiKey}` } }); if (!answer.ok) throw new Error(`the developer API answered ${answer.status}`); const app = (await answer.json()) as { redirect_uris: string[] }; return app.redirect_uris; } ``` **Python** ```python # The redirect addresses registered for the app this API key belongs to, for a deploy check. from aton_network import AtheronApi def registered_redirects(api_key: str) -> list: return AtheronApi(api_key).app()["redirect_uris"] ``` **Rust** ```rust // The redirect addresses registered for your API key's app, for a deploy check. use aton_network::{Api, Error}; pub async fn registered_redirects(api_key: &str) -> Result, Error> { let app = Api::new(api_key)?.app().await?; Ok(app.redirect_uris) } ``` **Go** ```go // The redirect addresses registered for your API key's app, for a deploy check. package apiapp import ( "context" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func RegisteredRedirects(ctx context.Context, apiKey string) ([]string, error) { api, err := atheron.NewAPI(apiKey) if err != nil { return nil, err } app, err := api.App(ctx) if err != nil { return nil, err } return app.RedirectURIs, nil } ``` **Java** ```java // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.java; import java.util.List; import org.atonnetwork.atheron.AtheronApi; public class RegisteredRedirects { public static List registeredRedirects(String apiKey) { return new AtheronApi(apiKey).app().redirectUris(); } } ``` **Kotlin** ```kotlin // The redirect addresses registered for your API key's app, for a deploy check. package examples.apiapp.kotlin import org.atonnetwork.atheron.AtheronApi fun registeredRedirects(apiKey: String): List = AtheronApi(apiKey).app().redirectUris() ``` **C#** ```csharp // The redirect addresses registered for your API key's app, for a deploy check. using AtonNetwork; namespace Examples.ApiApp; public static class RegisteredRedirects { public static async Task> RunAsync(string apiKey) { var app = await new AtheronApi(apiKey).AppAsync(); return app.RedirectUris ?? []; } } ``` **PHP** ```php app()['redirect_uris']; } ``` **Ruby** ```ruby # In a deploy check: confirm the redirect addresses your code expects are the ones registered. require "aton_network" def registered_redirects(api_key) AtonNetwork::Api.new(api_key).app["redirect_uris"] end ``` **Elixir** ```elixir # In a deploy check: confirm the redirect addresses your code expects are the ones registered. defmodule MyApp.DeployCheck do def registered_redirects(api_key) do with {:ok, app} <- api_key |> AtonNetwork.Api.new() |> AtonNetwork.Api.app() do {:ok, app["redirect_uris"]} end end end ``` ## Errors Every failure is an `%AtonNetwork.Error{}` with `code` (for example `invalid_grant`), `description`, the HTTP `status` and, on a 429, `retry_after`. See [Errors](/docs/errors) for every code. ## Frameworks [Phoenix](/docs/sdks/elixir/phoenix), which works the same way in any Plug app. --- # Phoenix (Elixir) URL: https://developers.aton-network.org/docs/sdks/elixir/phoenix Sign in with Atheron in Phoenix. **Coming soon.** `aton_network` is not published on Hex yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). `AtonNetwork.Plug` signs people in on the connection's session: the routes, the person in `conn.assigns.atheron_user` and a plug for pages that need somebody signed in. It works in Phoenix and in any Plug app (`forward "/auth", to: AtonNetwork.Plug.Routes`). ## Install ```elixir {:aton_network, "~> 0.1"} ``` Set `ATHERON_CLIENT_ID`, `ATHERON_CLIENT_SECRET` and `ATHERON_REDIRECT_URI` (the callback address you registered, such as `https://your.app/auth/callback`) on your server, or `config :aton_network, :auth, client_id: ..., redirect_uri: ...`. ## Add the routes **Next.js** ```ts // app/auth/[...atheron]/route.ts: every Sign in with Atheron route in one file. // Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_SECRET and // ATHERON_REDIRECT_URI=https://your.app/auth/callback (the address you registered). import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron(); export const { GET, POST } = atheron.handlers; ``` **React** ```tsx // The provider asks your server who is signed in (GET /auth/session); tokens never reach the browser. "use client"; import { AtheronProvider, SignInButton, useAtheron } from "@aton-network/react"; function Account() { const { user, status, signOut } = useAtheron(); if (status === "loading") return

Checking who you are

; if (!user) return ; return ; } export default function App() { return ( ); } ``` **Vue and Nuxt** ```ts // Nuxt: server/routes/auth/[...atheron].ts answers every Sign in with Atheron route. // defineEventHandler and toWebRequest are Nuxt's own, imported for you in server files. import { atheronNuxt } from "@aton-network/vue/nuxt"; const atheron = atheronNuxt(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export default defineEventHandler((event) => atheron.handle(toWebRequest(event))); ``` **SvelteKit** ```ts // src/hooks.server.ts: the routes under /auth, the session in locals.atheron, /account protected. import { atheronHandle } from "@aton-network/sveltekit"; export const handle = atheronHandle({ protect: ["/account"] }); // src/routes/+layout.server.ts: export const load = ({ locals }) => ({ user: locals.atheron?.user ?? null }); ``` **Angular** ```ts // app.config.ts: the service and a guard. Your server answers /auth (any Atheron server package). import { canActivateAtheron, provideAtheron } from "@aton-network/angular"; class AccountPage {} export const routes = [ { path: "account", component: AccountPage, canActivate: [canActivateAtheron] }, ]; export const appConfig = { providers: [provideAtheron({ basePath: "/auth" })] }; // In a component: readonly auth = inject(AtheronAuth); then auth.user(), auth.signIn(), auth.signOut() ``` **Express** ```ts // Sign in with Atheron for an Express app: the routes under /auth, and the person on every request. import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const app = express(); app.use(atheron.middleware); app.get("/", (req, res) => { const user = userOf(req); res.send(user ? `Hello ${user.name}` : `Sign in with Atheron`); }); ``` **NestJS** ```ts // main.ts: after NestFactory.create(AppModule) and before app.listen(). import { atheronNest } from "@aton-network/nestjs"; export const atheron = atheronNest(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export function withAtheron(app: T): T { app.use(atheron.middleware); // answers /auth/*, puts the session on req.atheron app.useGlobalGuards(atheron.guard); // or @UseGuards(atheron.guard) on the controllers that need it return app; } // In a controller: @Get("me") me(@Req() req) { return userOf(req); } ``` **React Router** ```ts // app/routes/auth.$.ts: the splat route answers every Sign in with Atheron route. import { atheronRouter } from "@aton-network/react-router"; export const atheron = atheronRouter(); // reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, ATHERON_REDIRECT_URI, ATHERON_SECRET export const loader = atheron.loader; export const action = atheron.action; // In any other loader: const user = await atheron.requireUser(request); ``` **Auth.js** ```ts // auth.ts: Atheron as an Auth.js provider. Register https://your.app/api/auth/callback/atheron. // Reads AUTH_ATHERON_ID and AUTH_ATHERON_SECRET unless you pass clientId and clientSecret. import Atheron from "@aton-network/authjs"; export const providers = [Atheron({ scopes: ["profile", "email"] })]; // next-auth: export const { handlers, auth, signIn, signOut } = NextAuth({ providers }); ``` **Passport** ```ts // Passport: the strategy keeps PKCE, state and nonce in the session (add express-session first). import { AtheronStrategy } from "@aton-network/passport"; export const strategy = new AtheronStrategy( { clientId: process.env.ATHERON_CLIENT_ID!, clientSecret: process.env.ATHERON_CLIENT_SECRET, callbackURL: "https://your.app/auth/atheron/callback", }, (tokens, profile, done) => done(null, { id: profile.sub, name: profile.name }), ); // passport.use(strategy); // app.get("/auth/atheron", passport.authenticate("atheron")); // app.get("/auth/atheron/callback", passport.authenticate("atheron", { failureRedirect: "/" }), (req, res) => res.redirect("/")); ``` **Django** ```python # urls.py: /auth/sign-in, /auth/callback, /auth/sign-out and /auth/session. # settings.py: add "aton_network.contrib.django.AtheronMiddleware" after SessionMiddleware, and set # ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI=https://your.app/auth/callback. from django.urls import include, path urlpatterns = [ path("auth/", include("aton_network.contrib.django.urls")), ] ``` **Flask** ```python # The routes under /auth, on Flask's session. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from flask import Flask from aton_network.contrib.flask import atheron_blueprint, current_user app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(atheron_blueprint(), url_prefix="/auth") @app.get("/") def home(): user = current_user() return f"Hello {user['name']}" if user else 'Sign in with Atheron' ``` **FastAPI** ```python # The routes under /auth, on Starlette's SessionMiddleware. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. import os from fastapi import Depends, FastAPI from starlette.middleware.sessions import SessionMiddleware from aton_network.contrib.fastapi import atheron_router, current_user app = FastAPI() app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], https_only=True) app.include_router(atheron_router(), prefix="/auth") @app.get("/") def home(user=Depends(current_user)): return {"signed_in": user is not None} ``` **Axum** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use std::sync::Arc; use aton_network::{Auth, Config, Error}; use aton_network_axum::MaybeUser; use axum::response::Html; use axum::routing::get; use axum::Router; pub fn site() -> Result<(Router>, Arc), Error> { let auth = Arc::new(Auth::from_env(Config::default())?); let app = Router::new().route("/", get(home)).merge(aton_network_axum::routes()); Ok((app, auth)) // add your routes, then app.with_state(auth) } async fn home(MaybeUser(user): MaybeUser) -> Html { match user { Some(user) => Html(format!("Hello {}", user.name().replace('<', "<"))), None => Html(r#"Sign in with Atheron"#.to_string()), } } ``` **Actix Web** ```rust // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). use actix_web::{web, HttpResponse}; use aton_network::{Auth, Config, Error}; use aton_network_actix::MaybeUser; pub fn auth_from_env() -> Result, Error> { Ok(web::Data::new(Auth::from_env(Config::default())?)) } pub fn site(cfg: &mut web::ServiceConfig, auth: web::Data) { cfg.app_data(auth.clone()).service(aton_network_actix::routes(auth)).route("/", web::get().to(home)); } async fn home(MaybeUser(user): MaybeUser) -> HttpResponse { let page = match user { Some(user) => format!("Hello {}", user.name().replace('<', "<")), None => r#"Sign in with Atheron"#.to_string(), }; HttpResponse::Ok().content_type("text/html; charset=utf-8").body(page) } ``` **Go** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func NewSite() (*http.ServeMux, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.Handle("/auth/", auth.Handler()) mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { if user := auth.CurrentUser(r); user != nil { fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) return } fmt.Fprint(w, `Sign in with Atheron`) }) return mux, auth, nil } ``` **Gin** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func NewGinRouter() (*gin.Engine, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } r := gin.Default() atherongin.Mount(r.Group("/auth"), auth) r.GET("/", func(c *gin.Context) { if user := atherongin.CurrentUser(c, auth); user != nil { c.String(http.StatusOK, "Hello %s", user.Name()) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`Sign in with Atheron`)) }) return r, auth, nil } ``` **Echo** ```go // The routes under /auth and a home page. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET, // ATHERON_REDIRECT_URI and ATHERON_SESSION_SECRET (32 random characters or more, for the session cookie). package quickstart import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func NewEchoServer() (*echo.Echo, *atheron.Auth, error) { auth, err := atheron.FromEnv(atheron.Config{}) if err != nil { return nil, nil, err } e := echo.New() atheronecho.Mount(e.Group("/auth"), auth) e.GET("/", func(c echo.Context) error { if user := atheronecho.CurrentUser(c, auth); user != nil { return c.String(http.StatusOK, "Hello "+user.Name()) } return c.HTML(http.StatusOK, `Sign in with Atheron`) }) return e, auth, nil } ``` **Spring Boot** ```java // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @GetMapping("/") public String home(@AtheronUser Map user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Spring Security signs people in with oauth2Login; the starter registers Atheron with PKCE on. // Register https://your.app/login/oauth2/code/atheron as a redirect address, and set ATHERON_CLIENT_ID, // ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI to it. package examples.quickstartsignin.springsecurity; import org.atonnetwork.atheron.Atheron; import org.atonnetwork.atheron.spring.AtheronSecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication @RestController public class SecureApplication { public static void main(String[] args) { SpringApplication.run(SecureApplication.class, args); } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients, Atheron atheron) throws Exception { return http .authorizeHttpRequests(a -> a.requestMatchers("/", "/webhooks/atheron").permitAll().anyRequest().authenticated()) .oauth2Login(AtheronSecurity.oauth2Login(registrations)) .logout(out -> out.addLogoutHandler(AtheronSecurity.revokeOnSignOut(clients, atheron.client())).logoutSuccessUrl("/")) .csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/atheron")) .build(); } @GetMapping("/") public String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "Sign in with Atheron"; return "Hello " + HtmlUtils.htmlEscape(user.getFullName()); } } ``` **Kotlin** ```kotlin // The starter brings the routes under /auth. Set ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and // ATHERON_REDIRECT_URI (or atheron.client-id, atheron.client-secret, atheron.redirect-uri). package examples.quickstartsignin.kotlin import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @SpringBootApplication @RestController class KotlinApplication { @GetMapping("/") fun home(@AtheronUser user: Map?): String = if (user == null) "Sign in with Atheron" else "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } fun main(args: Array) { runApplication(*args) } ``` **ASP.NET Core** ```csharp // The routes under /auth on cookie authentication. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET // and ATHERON_REDIRECT_URI (or the Atheron section of appsettings.json). using AtonNetwork.AspNetCore; namespace Examples.QuickstartSignIn; public static class Site { public static WebApplication Build(WebApplicationBuilder builder) { builder.Services.AddAtheron(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapAtheron(); app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Text($"Hello {http.User.Identity.Name}") : Results.Content("Sign in with Atheron", "text/html")); return app; } } ``` **Laravel** ```php Socialite::driver('atheron')->redirect())->name('login'); Route::get('/auth/callback', function () { try { $person = Socialite::driver('atheron')->user(); } catch (AtheronError $error) { return redirect('/?atheron_error=' . $error->error); } $user = User::updateOrCreate(['atheron_id' => $person->getId()], ['name' => $person->getName(), 'email' => $person->getEmail()]); Auth::login($user); return redirect()->intended('/account'); }); ``` **Symfony** ```php extension('atheron', [ 'client_id' => '%env(ATHERON_CLIENT_ID)%', 'client_secret' => '%env(ATHERON_CLIENT_SECRET)%', 'redirect_uri' => '%env(ATHERON_REDIRECT_URI)%', 'protect' => ['/account'], ]); }; ``` **WordPress** ```php Atheron shows it). define('ATHERON_CLIENT_ID', getenv('ATHERON_CLIENT_ID')); define('ATHERON_CLIENT_SECRET', getenv('ATHERON_CLIENT_SECRET')); define('ATHERON_WEBHOOK_SECRET', getenv('ATHERON_WEBHOOK_SECRET')); ``` **Rails** ```ruby # config/routes.rb: the routes under /auth. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. Rails.application.routes.draw do mount AtonNetwork::Engine, at: "/auth" get "account", to: "account#show" post "webhooks/atheron", to: "atheron_webhooks#create" end ``` **OmniAuth** ```ruby # config.ru (in Rails: config/initializers/omniauth.rb, with Rails.application.config.middleware.use OmniAuth::Builder). # POST /auth/atheron signs in; Atheron comes back to /auth/atheron/callback with env["omniauth.auth"]. require "omniauth/strategies/atheron" use OmniAuth::Builder do provider :atheron, ENV.fetch("ATHERON_CLIENT_ID"), ENV.fetch("ATHERON_CLIENT_SECRET"), scope: %w[profile email] end ``` **Phoenix** ```elixir # lib/my_app_web/router.ex: the routes under /auth, in the browser pipeline. Reads ATHERON_CLIENT_ID, ATHERON_CLIENT_SECRET and ATHERON_REDIRECT_URI. defmodule MyAppWeb.Router do use Phoenix.Router import Plug.Conn import Phoenix.Controller pipeline :browser do plug :accepts, ["html", "json"] plug :fetch_session plug :protect_from_forgery plug AtonNetwork.Plug.FetchUser end pipeline :signed_in do plug AtonNetwork.Plug.RequireUser end scope "/" do pipe_through :browser forward "/auth", AtonNetwork.Plug.Routes end scope "/", MyAppWeb do pipe_through [:browser, :signed_in] get "/account", AccountController, :show end scope "/", MyAppWeb do post "/webhooks/atheron", AtheronWebhookController, :create end end ``` The routes are `/auth/sign-in`, `/auth/callback`, `/auth/sign-out` (a POST from your own site, with the page's CSRF token) and `/auth/session`. Link to `/auth/sign-in?return_to=/account` to sign somebody in and bring them back. If you forward from somewhere else, set `config :aton_network, :mount_path` to match. ## Protect a page **Next.js** ```ts // proxy.ts (middleware.ts before Next 16): anybody not signed in goes to Atheron first, then back. import { Atheron } from "@aton-network/nextjs"; const atheron = Atheron({ protect: ["/account", "/settings"] }); export async function proxy(request: Request): Promise { return atheron.proxy(request); } // In a server component, read the person with: await atheron.user(await cookies()) ``` **React** ```tsx // A part of the page only a signed-in person sees; anybody else gets the button. "use client"; import type { ReactNode } from "react"; import { SignInButton, useAtheron } from "@aton-network/react"; export function SignedInOnly({ children }: { children: ReactNode }) { const { user, status } = useAtheron(); if (status === "loading") return null; return user ? <>{children} : ; } ``` **Vue and Nuxt** ```ts // main.ts: the plugin asks your server who is signed in; useAtheron() gives the person anywhere. import { createApp, defineComponent, h } from "vue"; import { AtheronSignInButton, createAtheron, useAtheron } from "@aton-network/vue"; const Account = defineComponent({ setup() { const atheron = useAtheron(); return () => (atheron.user ? h("p", `Hello ${atheron.user.name}`) : h(AtheronSignInButton, { returnTo: "/account" })); }, }); export const app = createApp(Account).use(createAtheron({ basePath: "/auth" })); if (typeof document !== "undefined") app.mount("#app"); ``` **Express** ```ts // requireUser: a signed-in person, or off to sign in (a 401 for requests that ask for JSON). import express from "express"; import { atheronExpress, userOf } from "@aton-network/express"; const atheron = atheronExpress(); export const app = express(); app.use(atheron.middleware); app.get("/account", atheron.requireUser, (req, res) => { res.json({ you: userOf(req)?.sub }); }); ``` **Django** ```python # views.py: a signed-in person, or off to sign in and back. from django.http import HttpResponse from aton_network.contrib.django import atheron_required @atheron_required def account(request): return HttpResponse(f"Hello {request.atheron_user['name']}") ``` **Flask** ```python # A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). from flask import Blueprint from aton_network.contrib.flask import current_user, login_required pages = Blueprint("pages", __name__) @pages.get("/account") @login_required def account(): return f"Hello {current_user()['name']}" ``` **FastAPI** ```python # A signed-in person, or 401. from fastapi import APIRouter, Depends from aton_network.contrib.fastapi import require_user router = APIRouter() @router.get("/account") def account(user: dict = Depends(require_user)): return {"you": user["sub"]} ``` **Axum** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use std::sync::Arc; use aton_network::Auth; use aton_network_axum::SignedIn; use axum::routing::get; use axum::Router; pub fn protect(app: Router>) -> Router> { app.route("/account", get(account)) } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Actix Web** ```rust // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). use actix_web::web; use aton_network_actix::SignedIn; pub fn protect(cfg: &mut web::ServiceConfig) { cfg.route("/account", web::get().to(account)); } async fn account(SignedIn(user): SignedIn) -> String { format!("Hello {}", user.name()) } ``` **Go** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "fmt" "html" "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Protect(mux *http.ServeMux, auth *atheron.Auth) { mux.Handle("/account", auth.RequireUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := atheron.UserFrom(r.Context()) fmt.Fprintf(w, "Hello %s", html.EscapeString(user.Name())) }))) } ``` **Gin** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atherongin "github.com/Atheron-ATON-Network/atheron-sdk-go/gin" "github.com/gin-gonic/gin" ) func ProtectGin(r *gin.Engine, auth *atheron.Auth) { r.GET("/account", atherongin.RequireUser(auth), func(c *gin.Context) { c.String(http.StatusOK, "Hello %s", atherongin.CurrentUser(c, auth).Name()) }) } ``` **Echo** ```go // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package protectapage import ( "net/http" "github.com/Atheron-ATON-Network/atheron-sdk-go" atheronecho "github.com/Atheron-ATON-Network/atheron-sdk-go/echo" "github.com/labstack/echo/v4" ) func ProtectEcho(e *echo.Echo, auth *atheron.Auth) { e.GET("/account", func(c echo.Context) error { return c.String(http.StatusOK, "Hello "+atheronecho.CurrentUser(c, auth).Name()) }, atheronecho.RequireUser(auth)) } ``` **Spring Boot** ```java // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.springboot; import java.util.Map; import org.atonnetwork.atheron.spring.AtheronRequired; import org.atonnetwork.atheron.spring.AtheronUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class AccountPage { @AtheronRequired @GetMapping("/account") public String account(@AtheronUser Map user) { return "Hello " + HtmlUtils.htmlEscape(String.valueOf(user.get("name"))); } } ``` **Spring Security** ```java // Anything the SecurityFilterChain marks authenticated: a signed-in person, or off to sign in and back. package examples.protectapage.springsecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @RestController public class SecureAccountPage { @GetMapping("/account") public String account(@AuthenticationPrincipal OidcUser user) { return "Hello " + HtmlUtils.htmlEscape(user.getFullName()) + " (" + user.getSubject() + ")"; } } ``` **Kotlin** ```kotlin // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). package examples.protectapage.kotlin import org.atonnetwork.atheron.spring.AtheronRequired import org.atonnetwork.atheron.spring.AtheronUser import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils @RestController class KotlinAccountPage { @AtheronRequired @GetMapping("/account") fun account(@AtheronUser user: Map): String = "Hello ${HtmlUtils.htmlEscape(user["name"].toString())}" } ``` **ASP.NET Core** ```csharp // A signed-in person, or off to sign in and back (a 401 for requests asking for JSON). using System.Security.Claims; namespace Examples.ProtectAPage; public static class AccountPage { public static void Map(WebApplication app) => app.MapGet("/account", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); } ``` **Laravel** ```php 'Hello ' . e(Auth::user()->name))->middleware('auth'); ``` **Symfony** ```php attributes->get('atheron_user'); return new Response('Hello ' . htmlspecialchars($user['name'])); } } ``` **WordPress** ```php { const raw = await request.text(); const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw) as WebhookEvent; if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } ``` **Next.js** ```ts // app/webhooks/atheron/route.ts: check the signature on the raw body, then act on the event. import { AtheronError, constructEvent } from "@aton-network/sdk"; export async function POST(request: Request): Promise { try { const event = await constructEvent(request.headers.get("atheron-signature"), await request.text(), process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "consent.revoked") { // Delete the refresh token you hold for event.data.sub, and stop using what you read. } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof AtheronError) return new Response(error.code, { status: 400 }); throw error; } } ``` **Express** ```ts // Express: read the raw body for this route only, check it, then act on the event. import express from "express"; import { constructEvent } from "@aton-network/sdk"; export const app = express(); app.post("/webhooks/atheron", express.raw({ type: "application/json" }), async (req, res) => { try { const event = await constructEvent(req.header("atheron-signature"), req.body as Buffer, process.env.ATHERON_WEBHOOK_SECRET!); if (event.type === "membership.changed") { // Read the new plan with a fresh access token; the event never carries it. } res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` **Python** ```python # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. import os from aton_network import AtheronError, construct_event def handle_webhook(signature: str, raw_body: bytes) -> int: try: event = construct_event(signature, raw_body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub, and stop using what you read. return 204 ``` **Django** ```python # views.py: the raw body, checked, then the event. Exempt from CSRF: Atheron signs it instead. import os from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from aton_network import AtheronError, construct_event @csrf_exempt @require_POST def atheron_webhook(request): try: event = construct_event(request.headers.get("Atheron-Signature"), request.body, os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return HttpResponse(status=400) if event.type == "membership.changed": pass # Read the new plan with a fresh access token; the event never carries it. return HttpResponse(status=204) ``` **Flask** ```python # The raw body, checked, then the event. import os from flask import Flask, request from aton_network import AtheronError, construct_event app = Flask(__name__) @app.post("/webhooks/atheron") def atheron_webhook(): try: event = construct_event(request.headers.get("Atheron-Signature"), request.get_data(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return "", 400 if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return "", 204 ``` **FastAPI** ```python # The raw body, checked, then the event. import os from fastapi import FastAPI, Request, Response from aton_network import AtheronError, construct_event app = FastAPI() @app.post("/webhooks/atheron") async def atheron_webhook(request: Request) -> Response: try: event = construct_event(request.headers.get("atheron-signature"), await request.body(), os.environ["ATHERON_WEBHOOK_SECRET"]) except AtheronError: return Response(status_code=400) if event.type == "consent.revoked": pass # Delete the refresh token you hold for event.sub. return Response(status_code=204) ``` **Rust** ```rust // Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. use aton_network::{construct_event, now_unix, Error, Event, DEFAULT_TOLERANCE}; pub fn from_atheron(signature_header: &str, raw_body: &[u8]) -> Result { let secret = std::env::var("ATHERON_WEBHOOK_SECRET").unwrap_or_default(); construct_event(signature_header, raw_body, &[&secret], DEFAULT_TOLERANCE, now_unix()) } ``` **Axum** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use std::sync::Arc; use aton_network::Auth; use aton_network_axum::Webhook; use axum::http::StatusCode; use axum::routing::post; use axum::Router; pub fn webhooks(app: Router>) -> Router> { app.route("/webhooks/atheron", post(webhook)) } async fn webhook(Webhook(event): Webhook) -> StatusCode { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } StatusCode::NO_CONTENT } ``` **Actix Web** ```rust // The raw body, checked against ATHERON_WEBHOOK_SECRET before the handler runs. A signature that // is not Atheron's answers 400; an error answers 500, so Atheron delivers it again. use actix_web::{web, HttpResponse}; use aton_network_actix::Webhook; pub fn webhooks(cfg: &mut web::ServiceConfig) { cfg.route("/webhooks/atheron", web::post().to(webhook)); } async fn webhook(Webhook(event): Webhook) -> HttpResponse { if event.kind() == "consent.revoked" { let _sub = event.sub(); // Delete the refresh token you hold for this person. } HttpResponse::NoContent().finish() } ``` **Go** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "context" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" ) func Webhooks(mux *http.ServeMux) { secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} mux.Handle("/webhooks/atheron", atheron.WebhookHandler(secrets, func(ctx context.Context, event atheron.Event) error { if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return nil })) } ``` **Gin** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/gin-gonic/gin" ) func GinWebhooks(r *gin.Engine) { r.POST("/webhooks/atheron", func(c *gin.Context) { body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, atheron.MaxWebhookBody)) if err != nil { c.Status(http.StatusBadRequest) return } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.GetHeader(atheron.SignatureHeader), body, secrets) if err != nil { c.Status(http.StatusBadRequest) return } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } c.Status(http.StatusNoContent) }) } ``` **Echo** ```go // The raw body, checked, then the event. A signature that is not Atheron's answers 400. package verifywebhook import ( "io" "net/http" "os" "github.com/Atheron-ATON-Network/atheron-sdk-go" "github.com/labstack/echo/v4" ) func EchoWebhooks(e *echo.Echo) { e.POST("/webhooks/atheron", func(c echo.Context) error { body, err := io.ReadAll(http.MaxBytesReader(c.Response(), c.Request().Body, atheron.MaxWebhookBody)) if err != nil { return c.NoContent(http.StatusBadRequest) } secrets := []string{os.Getenv("ATHERON_WEBHOOK_SECRET")} event, err := atheron.ConstructEvent(c.Request().Header.Get(atheron.SignatureHeader), body, secrets) if err != nil { return c.NoContent(http.StatusBadRequest) } if event.Type == "consent.revoked" { _ = event.Sub() // Delete the refresh token you hold for this person. } return c.NoContent(http.StatusNoContent) }) } ``` **Java** ```java // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.java; import java.util.List; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; public class HandleWebhook { public static int handleWebhook(String signatureHeader, byte[] rawBody) { Webhooks.Event event; try { event = Webhooks.constructEvent(signatureHeader, rawBody, List.of(System.getenv("ATHERON_WEBHOOK_SECRET"))); } catch (AtheronException e) { return 400; } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return 204; } } ``` **Spring Boot** ```java // The raw body, checked against atheron.webhook-secrets (or ATHERON_WEBHOOK_SECRET), then the event. package examples.verifywebhook.springboot; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { private final AtheronWebhooks webhooks; public WebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { Webhooks.Event event; try { event = webhooks.construct(signature, body); } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } return ResponseEntity.noContent().build(); } } ``` **Spring Security** ```java // The raw body, checked, then the event. In your SecurityFilterChain, permit /webhooks/atheron and // leave it out of CSRF (as the quickstart does): the signature is what proves it is Atheron. package examples.verifywebhook.springsecurity; import org.atonnetwork.atheron.AtheronException; import org.atonnetwork.atheron.Webhooks; import org.atonnetwork.atheron.spring.AtheronWebhooks; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class SecureWebhookController { private final AtheronWebhooks webhooks; public SecureWebhookController(AtheronWebhooks webhooks) { this.webhooks = webhooks; } @PostMapping("/webhooks/atheron") public ResponseEntity receive(@RequestHeader(name = Webhooks.SIGNATURE_HEADER, required = false) String signature, @RequestBody byte[] body) { try { Webhooks.Event event = webhooks.construct(signature, body); if (event.type().equals("consent.revoked")) { // Delete the refresh token you hold for event.sub(). } } catch (AtheronException e) { return ResponseEntity.badRequest().build(); } return ResponseEntity.noContent().build(); } } ``` **Kotlin** ```kotlin // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. package examples.verifywebhook.kotlin import org.atonnetwork.atheron.AtheronException import org.atonnetwork.atheron.Webhooks fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int { val event = try { Webhooks.constructEvent(signatureHeader, rawBody, listOf(System.getenv("ATHERON_WEBHOOK_SECRET"))) } catch (e: AtheronException) { return 400 } if (event.type() == "consent.revoked") { // Delete the refresh token you hold for event.sub(). } return 204 } ``` **C#** ```csharp // The raw body, checked, then the event. Answer 400 to a signature that is not Atheron's. using AtonNetwork; namespace Examples.VerifyWebhook; public static class HandleWebhook { public static int Run(string? signatureHeader, byte[] rawBody) { WebhookEvent e; try { e = Webhooks.ConstructEvent(signatureHeader, rawBody, [Environment.GetEnvironmentVariable("ATHERON_WEBHOOK_SECRET")]); } catch (AtheronException) { return 400; } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return 204; } } ``` **ASP.NET Core** ```csharp // The raw body, checked against Atheron:WebhookSecrets (or ATHERON_WEBHOOK_SECRET), then the event. using AtonNetwork; using AtonNetwork.AspNetCore; namespace Examples.VerifyWebhook; public static class WebhookRoute { public static void Map(WebApplication app) => app.MapPost("/webhooks/atheron", async (HttpRequest request, AtheronWebhooks webhooks) => { WebhookEvent e; try { e = await webhooks.ConstructAsync(request); } catch (AtheronException) { return Results.BadRequest(); } if (e.Type == "consent.revoked") { // Delete the refresh token you hold for e.Sub. } return Results.NoContent(); }); } ``` **PHP** ```php type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return 204; } ``` **Laravel** ```php header('Atheron-Signature'), $request->getContent(), (string) config('services.atheron.webhook_secret')); } catch (AtheronError) { return response()->noContent(400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return response()->noContent(); })->withoutMiddleware(ValidateCsrfToken::class); ``` **Symfony** ```php headers->get('Atheron-Signature'), $request->getContent(), $this->secret); } catch (AtheronError) { return new Response('', 400); } if ($event->type === 'consent.revoked') { // Delete the refresh token you hold for $event->sub(), and stop using what you read. } return new Response('', 204); } } ``` **WordPress** ```php sub()); if ($event->type === 'consent.revoked' && $user) { // They took back what your app may read: forget what you copied from Atheron. update_user_meta($user->ID, 'atheron_consent_revoked_at', $event->created); } }); ``` **Ruby** ```ruby # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. require "aton_network" def handle_webhook(signature, raw_body) event = AtonNetwork::Webhooks.construct_event(signature, raw_body, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end 204 rescue AtonNetwork::Error 400 end ``` **Rails** ```ruby # app/controllers/atheron_webhooks_controller.rb: the raw body, checked before anything acts on it. class AtheronWebhooksController < ActionController::API def create event = AtonNetwork::Webhooks.construct_event(request.headers["Atheron-Signature"], request.raw_post, ENV.fetch("ATHERON_WEBHOOK_SECRET")) if event.type == "consent.revoked" # Delete the refresh token you hold for event.sub, and stop using what you read. end head :no_content rescue AtonNetwork::Error head :bad_request end end ``` **Elixir** ```elixir # Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received. defmodule MyApp.Webhooks do def handle_webhook(signature, raw_body) do case AtonNetwork.Webhooks.construct_event(signature, raw_body, System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. 204 {:ok, _event} -> 204 {:error, _} -> 400 end end end ``` **Phoenix** ```elixir # lib/my_app_web/controllers/atheron_webhook_controller.ex: the raw body, kept by AtonNetwork.Plug.RawBody as the # endpoint's Plug.Parsers body_reader, checked before anything acts on it. defmodule MyAppWeb.AtheronWebhookController do use Phoenix.Controller, formats: [:json] import Plug.Conn def create(conn, _params) do signature = conn |> get_req_header("atheron-signature") |> List.first() case AtonNetwork.Webhooks.construct_event(signature, conn.assigns[:raw_body] || "", System.fetch_env!("ATHERON_WEBHOOK_SECRET")) do {:ok, %{type: "consent.revoked"} = _event} -> # Delete the refresh token you hold for AtonNetwork.Event.sub(event), and stop using what you read. send_resp(conn, 204, "") {:ok, _event} -> send_resp(conn, 204, "") {:error, _} -> send_resp(conn, 400, "") end end end ``` In `endpoint.ex`, give `Plug.Parsers` the body reader `{AtonNetwork.Plug.RawBody, :read_body, []}` so the raw body is kept in `conn.assigns.raw_body`: the signature is over the bytes Atheron sent. PKCE, `state` and `nonce` are checked on every sign-in, the flow is spent on use and lasts ten minutes, the session is a new one at sign-in, a return address is always a path on your site, and signing out revokes the refresh token at Atheron. --- # The atheron CLI URL: https://developers.aton-network.org/docs/sdks/cli Sign in, list apps, rotate secrets and forward webhooks to localhost from your terminal. **Coming soon.** `@aton-network/cli` is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from [Endpoints](/docs/reference). The `atheron` command signs you in with your Atheron account, lists your apps, rotates client secrets, and brings your test environment's webhooks to your own machine. ## Install ```bash npm install --global @aton-network/cli ``` ## Sign in ```bash atheron login ``` Your browser opens on this portal. Check that the pairing code it shows is the one in your terminal, then confirm. The CLI makes its token on your machine and never sends it through the browser: only its hash travels, and the portal answers your machine's own address (`127.0.0.1`) with an activation only that token can use. So a link somebody else made cannot sign you in on their machine. The sign-in lasts 14 days, sits in a file only you can read, and ends with `atheron logout`. In CI, set `ATHERON_CLI_TOKEN` instead. ## Your apps ```bash atheron apps list atheron apps get "Price Check" # an app's ID, a client ID or its name ``` ## Rotate a client secret ```bash atheron secrets rotate "Price Check" --env live --revoke-old ``` The new secret is shown once. With `--revoke-old` the environment's other secrets stop working right after the new one exists, so there is no gap. Without it they keep working until you run `atheron secrets revoke `. Owners and admins rotate live secrets; developers rotate test ones too. ## Webhooks on your machine ```bash atheron webhooks listen "Price Check" --forward-to http://localhost:3000/webhooks atheron webhooks trigger "Price Check" user.signed_in ``` `listen` prints a signing secret for the session and signs every event it forwards with it, exactly as we sign a real delivery: the same body, `Atheron-Signature` and `Atheron-Event-Id`. Point your handler's secret at it while you test. It needs no public address. It forwards the test environment's events from the moment you start; `trigger` makes a test event (marked `"test": true`, for a made-up person), thirty a minute at most. ## Send past deliveries again ```bash atheron webhooks deliveries "Price Check" --state gave-up atheron webhooks replay "Price Check" 812 813 atheron webhooks replay "Price Check" --failed --env test ``` `deliveries` lists the last thirty days of deliveries, newest first, with the resends among them (`--env`, `--state waiting|delivered|gave-up`, `--limit` up to 100). `replay` sends the deliveries you name again, exactly as they were: the same body and the same `Atheron-Event-Id`, so a handler that drops repeats still can, signed at the moment it goes. `--failed` resends the ones that gave up and have not been resent yet. Each resend is tried once, so you see straight away whether your endpoint took it. Up to 25 in one go and 60 an hour for an app; live deliveries need the owner or admin role. Every resend is in the app's Activity, and the portal's Webhooks tab does the same. ## Other tools | Command | What it does | |---|---| | `atheron whoami` | Who this CLI is signed in as | | `atheron tokens decode ` | A token's header and claims, with readable times (no signature check) | | `atheron webhooks verify --secret ... --header ... --body-file ...` | Whether a delivery you saved is genuine | | `atheron api app`, `atheron api usage --days 7` | The developer API, with `ATHERON_API_KEY` | | `atheron api graphql ''` or `@file.graphql` `--variables '{...}'` | One GraphQL request, with `ATHERON_API_KEY`; exits 1 when a field failed | | `atheron open docs` | This site in your browser | Every command takes `--json`. Exit codes: 0 done, 1 refused or failed, 2 a mistake in the command, 3 not signed in.