For developers · OpenID Connect
Sign in with Skysmind.
On anything you build.
Let people sign in to your website, app or service with the Skysmind account they already use for Nuvira. It's standard OAuth 2 and OpenID Connect, so it works with the library you already have, and it's private by design: you get a stable ID and a username, never an email, and never anything from their messages.
https://auth.skysmindtech.com/.well-known/openid-configurationAuthorization code + PKCEHow it works
- Your appsends people to Skysmind with your
client_id - Skysmindsigns them in (password, or a QR they approve in Nuvira) and asks if they want to continue to you
- Back to youwith a one-time
codethat lasts 60 seconds - Your serverswaps the code for an
id_tokenand anaccess_token
Each app sees its own ID for a person (sub), the same every time. Two services can't match their users up through Skysmind.
A username and picture with profile, their Nuvira ID with nuvira_id. Never an email or a phone number.
Nuvira Family child accounts can't sign in to apps outside Skysmind. Your sign-in screen says plainly that Skysmind didn't make your app.
Register your app
You need a Skysmind account. Every app gets a client_id; a server that can keep a secret also gets a client_secret, shown once.
Hook it in
Pick the one that matches how your service works. They all end the same way: you know who signed in, by a stable sub.
Give your library the issuer. It finds everything else from /.well-known/openid-configuration.
// auth.ts
import NextAuth from "next-auth"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [{
id: "skysmind",
name: "Skysmind",
type: "oidc",
issuer: "https://auth.skysmindtech.com",
clientId: process.env.SKYSMIND_CLIENT_ID,
clientSecret: process.env.SKYSMIND_CLIENT_SECRET,
authorization: { params: { scope: "openid profile" } },
checks: ["pkce", "state", "nonce"],
profile: (p) => ({ id: p.sub, name: p.preferred_username, image: p.picture }),
}],
})
// Return URL to register: https://your.site/api/auth/callback/skysmindimport * as client from "openid-client"
const config = await client.discovery(
new URL("https://auth.skysmindtech.com"),
process.env.SKYSMIND_CLIENT_ID,
process.env.SKYSMIND_CLIENT_SECRET,
)
// 1. Send them to Skysmind
const verifier = client.randomPKCECodeVerifier()
const state = client.randomState()
const url = client.buildAuthorizationUrl(config, {
redirect_uri: "https://your.site/callback",
scope: "openid profile",
code_challenge: await client.calculatePKCECodeChallenge(verifier),
code_challenge_method: "S256",
state,
})
// keep verifier + state in the session, then redirect to url
// 2. On https://your.site/callback
const tokens = await client.authorizationCodeGrant(config, currentUrl, {
pkceCodeVerifier: verifier,
expectedState: state,
})
const { sub, preferred_username } = tokens.claims()import passport from "passport"
import { Strategy } from "passport-openidconnect"
passport.use("skysmind", new Strategy({
issuer: "https://auth.skysmindtech.com",
authorizationURL: "https://auth.skysmindtech.com/oauth/authorize",
tokenURL: "https://auth.skysmindtech.com/oauth/token",
userInfoURL: "https://auth.skysmindtech.com/oauth/userinfo",
clientID: process.env.SKYSMIND_CLIENT_ID,
clientSecret: process.env.SKYSMIND_CLIENT_SECRET,
callbackURL: "https://your.site/auth/skysmind/callback",
scope: ["profile"],
state: true,
}, (issuer, profile, done) => done(null, { id: profile.id, username: profile.username })))Most frameworks have a "generic OpenID Connect" option: Spring Security, ASP.NET, Django (mozilla-django-oidc), Laravel Socialite, Rails (omniauth_openid_connect), Supabase and Clerk custom providers, Keycloak or Auth0 as an upstream identity provider. Use these values:
| Issuer / discovery | https://auth.skysmindtech.com |
| Scopes | openid profile (add nuvira_id if you need it) |
| Client auth | client_secret_basic, client_secret_post, or none + PKCE |
| ID token | ES256, keys at /oauth/jwks |
| User ID | sub (don't key accounts on the username) |
A browser-only app (no secret) can use our script. Register it as a browser or phone app, and use the page the button lives on as the return URL.
<script src="https://auth.skysmindtech.com/sdk.js"></script>
<div id="skysmind"></div>
<script>
const opts = {
clientId: "app_…", // from step 02
redirectUri: "https://your.site/login", // this page, registered exactly
scope: "openid profile",
}
Skysmind.button(document.getElementById("skysmind"), opts)
Skysmind.handleRedirect(opts).then((session) => {
if (!session) return
console.log(session.user.sub, session.user.preferred_username)
// send session.idToken to your backend and verify it there
})
</script>
No library? It's three requests. Register a server app to get a secret.
// 1. Send them here (keep state in their session)
const url = new URL("https://auth.skysmindtech.com/oauth/authorize")
url.search = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: "https://your.site/callback",
response_type: "code",
scope: "openid profile",
state,
})
// 2. On /callback?code=…&state=… (check state first)
const res = await fetch("https://auth.skysmindtech.com/oauth/token", {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
authorization: "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`),
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://your.site/callback",
}),
})
const { access_token, id_token } = await res.json()
// 3. Who is it?
const me = await fetch("https://auth.skysmindtech.com/oauth/userinfo", {
headers: { authorization: `Bearer ${access_token}` },
}).then((r) => r.json())
// { sub: "sk_…", preferred_username: "sky", picture: "…" }import requests
# 2. On /callback?code=…&state=… (check state first)
tokens = requests.post(
"https://auth.skysmindtech.com/oauth/token",
auth=(CLIENT_ID, CLIENT_SECRET),
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": "https://your.site/callback",
},
).json()
# 3. Who is it?
me = requests.get(
"https://auth.skysmindtech.com/oauth/userinfo",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
).json()
print(me["sub"], me.get("preferred_username"))curl https://auth.skysmindtech.com/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://your.site/callback
curl https://auth.skysmindtech.com/oauth/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"Register a browser or phone app with your own scheme as the return URL, like com.example.app:/skysmind. Use the system browser sheet, never a web view, and always PKCE. AppAuth does all of it.
import AppAuth
let issuer = URL(string: "https://auth.skysmindtech.com")!
let config = try await OIDAuthorizationService.discoverConfiguration(forIssuer: issuer)
let request = OIDAuthorizationRequest(
configuration: config,
clientId: "app_…",
scopes: [OIDScopeOpenID, OIDScopeProfile],
redirectURL: URL(string: "com.example.app:/skysmind")!,
responseType: OIDResponseTypeCode,
additionalParameters: nil) // PKCE is added for you
authFlow = OIDAuthState.authState(byPresenting: request, presenting: viewController) { state, error in
let idToken = state?.lastTokenResponse?.idToken
}val issuer = Uri.parse("https://auth.skysmindtech.com")
AuthorizationServiceConfiguration.fetchFromIssuer(issuer) { config, _ ->
val request = AuthorizationRequest.Builder(
config!!, "app_…", ResponseTypeValues.CODE,
Uri.parse("com.example.app:/skysmind"),
).setScopes("openid", "profile").build() // PKCE is added for you
startActivityForResult(
AuthorizationService(this).getAuthorizationRequestIntent(request), RC_AUTH)
}Build a sign-in link
Fill in your app and watch the link build itself. "Try it" walks through the real screens; your return URL has to be registered first.
Reference
Endpoints
| What | Where |
|---|---|
| Discovery | GET /.well-known/openid-configuration |
| Authorize | GET /oauth/authorize |
| Token | POST /oauth/token |
| User info | GET /oauth/userinfo |
| Signing keys | GET /oauth/jwks |
| Revoke | POST /oauth/revoke |
Scopes and claims
| Scope | You get |
|---|---|
openid | sub and an id_token. sub starts with sk_ and is unique to your app. |
profile | preferred_username, and picture when they have one |
nuvira_id | nuvira_id, the sm number friends add them with |
Good to know
- Return URLs match exactly. https anywhere; http only on localhost; or your app's own scheme with a dot in it. No wildcards.
- PKCE is required for browser and phone apps. Server apps may use it too; we recommend it.
- Codes last 60 seconds and work once. Access tokens last 30 days. ID tokens last an hour.
- Usernames can change hands in theory and aren't secret. Key your users on
sub. - People can disconnect you any time from their account page. Their tokens stop working right away.
- Names can't use "Skysmind" or "Nuvira". People need to know who they're signing in to.
- Errors come back to your return URL as
?error=access_denied(they said no),invalid_scopeorinvalid_request.
The button
Use ours from sdk.js, or draw your own: the three bars, "Sign in with Skysmind", ink on cream or cream on ink. Please don't change the bars or the words.