{"version":"v1.0","client_state":{"DocsNavigation.default":{"fields":{"query":""},"persist":"memory","send_to_server":true},"ColorScheme.default":{"fields":{"mode":"system"},"persist":"local","send_to_server":true}},"endpoints":{"action":"/_bretzel/action","sse":"/_bretzel/sse","refetch":"/_bretzel/refetch"},"csrf":"ae5867c0270af52495daff3853211712","page_id":"72483fd54a234d0998a1bb75eb2fa6f5","address":""}

Authentication

Bretzel holds the identity and its transport. Your app holds the proof — and that is the only decision that is really yours: accepting this person, or not.

Who does what

What it isWho carries it
the signed cookie, its expiry, its anti-fixation rotationBretzel
where an identity can come from (`@auth.source`)Bretzel — you wire the source
the OAuth2 / OIDC doors (`@auth.door`)Bretzel — route, token exchange, cookie
the `UserState` scopeBretzel — it exists ONLY through that auth
the passwords, the account table, the rolesyour app
the sign-in pageyour app

Signing in with a Google account

An OIDC door is declared, and the decorator mounts its route, exchanges the token and sets the cookie. What is left to write is the decision: returning `None` REFUSES.
from bretzel import auth, oauth

@auth.door(oauth.OIDC(
    name="google",
    issuer="https://accounts.google.com",
    client_id=CLIENT_ID,
    client_secret=CLIENT_SECRET,
))
def way_in(profile) -> str | None:
    """The ONLY filter between “this person has a
    Google account” and “this person comes into my
    app”. Without it, the door is open to the whole
    planet."""
    if not profile.email.endswith("@mycompany.com"):
        return None
    return profile.email     # the identifier kept
`@auth.door` STACKS: two doors can end at the same function, which is the common case when one accepts Google AND Microsoft. `oauth.OAuth2` covers the providers with no OIDC, where the three URLs are given by hand.

Without OAuth: the identity source

`@auth.source` says WHERE an identity can come from — an API token, a header set by a trusted proxy, a home-made session. The function returns an identifier, or `None`.
@auth.source
def from_the_token(request) -> str | None:
    token = request.headers.get("x-api-key")
    return accounts.by_token(token)      # or None

# After a password check, over to you:
auth.login(user.id)
auth.logout()
auth.user_id()          # str | None
auth.is_authenticated() # bool

The scope that depends on it

`UserState` is the only one of the four server scopes that demands an identity. That is what made the split confusing: an app resolving its own user, without going through `auth`, received a 401 on touching that scope.
class Preferences(UserState):
    theme: str = field(default="light")

# Readable as soon as `auth.user_id()` returns something.
Preferences().theme = "dark"

What is NOT shipped

No sign-in page supplied, no password handling, no roles and no permissions, no SAML. And not Apple: its `client_secret` is an ES256 JWT, hence a cryptographic dependency the framework refuses to add for one provider.`examples/auth` shows the four ways in side by side, and it runs.