# 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
;
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