README
¶
Auth Service
This is technically a full stack app. We are building it to conform to the way we have built the other HTTP services,
which are all APIs, to maintain our patterns. At a future date, we will refactor our fx deps and the cmds. For now, we
respect convention.
Prior Art
This service borrows heavily from https://github.com/vouch/vouch-proxy.
Configs
New env vars for auth.nuon.co
| Env var | Description | Scope |
|---|---|---|
| ROOT_DOMAIN | The root domain for all the services. e.g. byoc.org.co. | All Services |
| NUON_AUTH_SESSION_KEY | Session Key for nonce | Auth Service |
| NUON_AUTH_ALLOWED_DOMAINS | Domains from which any user can register. Usually the org email. | Auth Service |
| NUON_AUTH_ALLOW_ALL_USERS | Whether or not any user with an email from an allowed domain should be allowed to register. | Auth Service |
We construct a domain from the root domain by prefixing auth to it. e.g. app.byoc.org.co. This becomes the service
domain which the service understands itself to be available at .
[!IMPORTANT] These values are required. If they do are not present, the service will not start.
[!NOTE]
NUON_AUTH_ALLOW_ALL_USERSdefaults to false.
[!NOTE]
NUON_AUTH_ALLOWED_DOMAINScannot be empty.
Identity Providers
This service depends on a the table identity_providers and on the default identity provider configured via env vars.
Configs
There are two sources for configurations.
- Env vars: a default provider must be provided via env vars. If these configs are not provided, the service will not start up.
- Database: additionaly
IdentityProviders can be created. These store the configs in a json column.
Env Var Configs
| Env var | Description |
|---|---|
| nuon_auth_provider_type | one of 'oidc', 'google', 'github' |
| nuon_auth_client_id | Client ID |
| nuon_auth_client_secret | Client Secret |
| nuon_auth_issuer_url | Issuer URL |
| nuon_auth_redirect_url | Redirect URL |
| nuon_auth_provider_name | Optional label shown on the sign-in page. Defaults to a type-derived |
| name ('Single Sign-On', 'Google', 'GitHub'). |
Examples
For these examples, assume the BYOC Nuon is deployed with the following ROOT_DOMAIN: byoc.org.co.
Auth0 as a generic OIDC provider
| Env var | Value |
|---|---|
| nuon_auth_provider_type | 'oidc' |
| nuon_auth_client_id | [secret] |
| nuon_auth_client_secret | [secret] |
| nuon_auth_issuer_url | https://audience.auth0.com |
| nuon_auth_redirect_url | auth.byoc.org.co/auth |
Google as an OAuth provider
| Env var | Value |
|---|---|
| nuon_auth_provider_type | 'google' |
| nuon_auth_client_id | [secret] |
| nuon_auth_client_secret | [secret] |
| nuon_auth_redirect_url | auth.byoc.org.co/auth |
in-database configs
If additional IdPs must be created, we need to add to the identity_providers table. At the time of writing, only
install-wide configs are supported. This means any providers added this way will be available to all users of the
install. As such, these are created via and admin api endpoint.
Future: in the future we'll add support for org-specific IdPs. At that point, an org-specific endpont on the public api may be added.
Examples
Auth0 as a generic OIDC provider
{
"enabled": true,
"name": "Acme SSO",
"allow_all_users": false,
"provider_type": "oidc",
"openid_config": {
"client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"client_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"issuer_url": "https://audience.auth0.com",
"redirect_url": "http://auth.byoc.org.co/auth"
}
}
Github
{
"enabled": true,
"provider_type": "github",
"github_config": {
"client_id": "XXXXXXXXXXXXXXXXXXXX",
"allowed_orgs": ["999999999"],
"redirect_url": "https://auth.byoc.org.co/auth",
"allowed_teams": ["1111111"],
"client_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
Default scopes are, user:email and read:user.
[!IMPORTANT] If a user does not have a public email, they will not be able to authenticate.
[!IMPORTANT] If a user's primary email does not have an allowed domain, they will not be able to authenticate.
Loading Order
The default provider is required for startup. this default provider is composed from the provider configs loaded from the env files.
After startup, we load the rest of the providers with a helper called getIdentityProviders. This helper loads the
default provider from the env and then loads any additional providers from the database, oldest first. That ordering is
the sign-in page's button order, so the env provider is always the first button.
Providers are identified by their ID. The env provider has no database row, so it gets the synthetic ID
default-<provider_type> (app.EnvIdentityProviderID); database providers use their row ID. /login?provider=<id>
still accepts a bare provider type for links minted before providers became individually addressable, which resolves to
the first enabled provider of that type.
Provider Lookup
| Function | Purpose |
|---|---|
getIdentityProvider(ctx, ref) |
Main entry point - resolves an ID, falling back to a bare type |
getIdentityProviderByType(ctx, ProviderType) |
Looks up the first IdentityProvider of a type (env first, then DB) |
getDefaultIdentityProvider() |
Builds IdentityProvider from env vars only |
createProviderFromIdentityProvider(ip) |
Converts IdentityProvider model to Provider interface |
Uniqueness
Any number of providers of a given type can be configured: one through env vars, the rest in the database. Two OIDC providers (say Auth0 for staff and Entra for contractors) is the case this exists for. Registering the same application twice - same type, client ID and, for OIDC, issuer - is rejected with a 409.
Because several providers can share a type, provider_type no longer identifies a provider. account_identities is
keyed on (identity_provider_id, sub) and (account_id, identity_provider_id) instead, and identity_provider_id
carries the env sentinel for accounts that signed in through the env provider. It is deliberately not a foreign key:
the env provider has no row, and ON DELETE SET NULL would collapse a deleted provider's identities onto each other.
nuon_auth_allow_all_users is deployment-wide, which cannot express "staff IdP open to
self-signup, contractor IdP invite-only". A provider may set allow_all_users to override it;
leaving it unset keeps the deployment-wide behaviour.
Creating or updating a DB-configured OIDC provider runs discovery against the issuer and rejects the request if it fails. Env providers get that check at boot; without this a typo in the issuer surfaced as a 500 on someone's first login rather than an error for the admin who typed it.
Give each provider a name when there is more than one. Without one, the sign-in page falls back to a type-derived
label, and for OIDC it shows the issuer host underneath so two unnamed OIDC buttons are still distinguishable.
Internals
Templates
This is the only service that is using templates. Even then, all templates are "namespaced" with auth so they are all
registered with this naming convention auth/*.tmpl. The files are all named *.html.tmpl.
Middleware
This service does need or know about the Org or Auth middleware in use by the other API services. This service has only a few endpoints so it doesn't actually implement an auth middleware for its own use and instead relies on the session.
Session
The auth service maintains a short lived session, nuon-auth-session, to store the next url (final destination) and the
provider type. This session is deleted as soon as the auth flow is complete and is only ever available to the auth
service itself. The service uses the NUON_AUTH_SESSION_KEY to sign the cookie for the session (HMAC-SHA256).
Cookie
When auth is complete, the auth provider sets a cross domain cookie X-Nuon-Auth that is accessible to any service on
ROOT_DOMAIN (e.g. byoc.org.co). This cookie is invalidated when a user visits /logout.
Forthcoming
SAML
SAML support will be built into this service.
SCIM
SCIM support will be built into this service.
Runner Auth
We have a method to identify the runner via a presigned sts get caller identity call.
- Runner creates a presigned STS GetCallerIdentity request (doesn't send it)
- Runner sends the presigned request details to your service
- Your service makes the actual STS call using those headers
- AWS validates the signature and returns the caller identity
- Your service validates the identity matches expected runner
Additional Claims
We'd like to add support for additional claims so the IdP can provide additional details about the user. This is a future-looking feature leaving room for the concept of roles and org access.
| attr | desc |
|---|---|
| groups | list of strings where each string is a group such as admin or developer |
| orgs | list of strings where each string is, ideally, an org id or an org slug |
Caveats & Limitations
Changing a Provider of a Given Type
Changing an env-var provider to a different provider of the same type is likely to cause issues. At this time, this is not explicitly supported. This applies to both, env-var based IdPs and in-database IdPs. Adding a second provider instead of repointing the first one avoids this entirely.
Preflight
nctl preflight validates the env-configured provider, including real OIDC discovery against the issuer. It cannot see
database-configured providers at all.
CLI Flow
To login via the CLI:
nuon auth login
This will pull down the cli configs from the api. If Nuon Auth is enabled, it will use the auth service for authentication.
Once the flow starts, the CLI will open the /device/code endpoint. If the user is not authenticated, the endpoint will
redirect the user to the index page with the s.cfg.Domain + /device/code as the url param. If the user is
authenticated, the device code flow will start.
[!NOTE] The /device/code endpoint should be opened directly, otherwise the user may invalidate their session for the browser.
Flow
CLI Browser Server
| | |
|-- Generate device_code locally ---->| |
|-- Open browser /device/code?code=XXX ------------------------------------->|
| |<-- Check X-Nuon-Auth cookie ---------|
| |<-- Show "Approve CLI?" page ---------|
| |-- User clicks Approve (POST) ------->|
| | (stores code → account_id)
| |<-- Show success page ----------------|
|-- Poll GET /device/token?code=XXX ---------------------------------------->|
|<-- 200 {access_token: "tok..."} -------------------------------------------|