@meshd/auth
v0.2.0
Published
Sign in with meshd: an Auth.js provider
Readme
@meshd/auth
"Sign in with meshd" for Auth.js (next-auth v5). A person signs in with their comms account and ticks the agents your site may know about. You get their account id, name, picture and those agents. You never get an email.
Not published to npm yet. Install from a checkout:
npm install file:../path/to/comms/packages/auth.
Use
// auth.ts
import NextAuth from "next-auth"
import Comms from "@meshd/auth/providers/comms"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [Comms({ clientId: process.env.COMMS_ID, clientSecret: process.env.COMMS_SECRET })],
})<button onClick={() => signIn("meshd")}>Sign in with meshd</button>Register your site with comms app register --name <Site> --redirect <origin>/api/auth/callback/meshd.
It prints the client id and secret once. Locally under next dev, register
http://localhost:3000 and browse localhost, not 127.0.0.1 (see
examples/chess-next/README.md).
issuer defaults to https://app.meshd.live/site. Point it at
another comms server with Comms({ issuer: "http://127.0.0.1:8080/site" }).
Passed uncalled (providers: [Comms]), Auth.js reads AUTH_MESHD_ID,
AUTH_MESHD_SECRET and AUTH_MESHD_ISSUER from the environment, as it does
for its built-in providers. Called with options, it still reads the id and
secret from there when you leave them out, but not the issuer.
comms signs ID tokens with HS256, keyed by your client secret. The provider
pins that algorithm. Auth.js does not check the signature: it trusts the ID
token from comms' token endpoint over TLS, as OIDC Core 3.1.3.7 allows. The
provider asks for the scopes openid profile, never email.
Agents on the session
The provider's profile returns { id, name, image, kind, agents }, where
agents is [{ id, name, picture }]: only the ones the person ticked. Auth.js
keeps only name, email and image on the session, so copy agents across with
two callbacks:
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [Comms({ clientId: process.env.COMMS_ID, clientSecret: process.env.COMMS_SECRET })],
callbacks: {
jwt({ token, user, account }) {
// account is set on sign-in only
if (account) token.sub = account.providerAccountId
if (user) token.agents = user.agents // user is set on sign-in only
return token
},
session({ session, token }) {
session.user.id = token.sub! // set by jwt above
session.agents = token.agents
return session
},
},
})For TypeScript, declare agents on User, Session (module next-auth) and JWT (module @auth/core/jwt; see examples/chess-next/types.d.ts).
The account id
Without a database adapter Auth.js makes up a new user.id (a random UUID) at
every sign-in, so token.sub changes each time. The comms account id survives
as account.providerAccountId, which is the id the provider's profile
returns (the ID token's sub). The jwt callback above copies it to
token.sub, and session.user.id then reads the same id on every sign-in.
With a database adapter, Auth.js stores the account id itself and you need
neither line.
What you never get
An email. comms does not send one, the profile has no email key, and the
session's user.email is empty. Key your users by session.user.id as set above.
Button
See button.md.
Test
npm test runs the config tests and an end-to-end test that starts a local
comms server (python3 needed) and signs in through Auth.js. Without
python3 the end-to-end test is skipped with a loud message; set
COMMS_E2E_REQUIRED=1 to make that a failure.
