Introduction
"Sign in with 4S" is an OAuth 2.0 / OpenID Connect (OIDC) compliant authentication feature. External applications can let users sign in with their 4S account and access the 4S read (GET) APIs on their behalf.
Because the implementation is standards-compliant, common OIDC libraries such as NextAuth (Auth.js) and openid-client work out of the box. The only supported flow is Authorization Code with PKCE (S256).
Register your app from the Developer menu on 4S (https://4s.link/developer). You will receive a client_id and a client_secret. The client_secret is shown only once at registration — store it securely.
Basics
| Item | Value |
|---|---|
| Issuer | https://api.4s.link |
| Supported flow | Authorization Code + PKCE (S256) |
| Client authentication | client_secret_basic / client_secret_post / none (public + PKCE) |
| ID Token signature | RS256 |
| Access token lifetime | 1 hour |
| Refresh token lifetime | 60 days (rotating) |
# OIDC Discoveryhttps://api.4s.link/.well-known/openid-configuration# JWKS (公開鍵)https://api.4s.link/.well-known/jwks.json# 認可エンドポイントhttps://4s.link/oauth/authorize# トークンエンドポイントhttps://api.4s.link/oauth/token# UserInfohttps://api.4s.link/oauth/userinfo# トークン失効https://api.4s.link/oauth/revoke
Authentication Flow
The Authorization Code + PKCE flow works as follows: (1) your app generates a code_verifier and redirects the user to the authorization endpoint with its S256 hash (code_challenge); (2) the user signs in to 4S and grants consent, and an authorization code is returned to your redirect_uri; (3) your app exchanges the code together with the code_verifier for tokens at the token endpoint.
https://4s.link/oauth/authorizeThe authorization endpoint is a URL you redirect the user's browser to (it is not an API). Attach the following query parameters.
Query parameters
response_typestringrequiredMust be code
client_idstringrequiredClient ID issued in the Developer menu
redirect_uristringrequiredMust exactly match a registered redirect URI
statestringRandom value for CSRF protection, returned unchanged in the callback (strongly recommended)
noncestringRandom value returned as the nonce claim in the ID Token (recommended)
code_challengestringrequiredbase64url-encoded SHA-256 hash of the code_verifier
code_challenge_methodstringrequiredMust be S256
promptstringSet to consent to force the consent screen even if already granted. none is not supported and returns interaction_required
After the user approves, the browser returns to your redirect_uri with ?code=...&state=.... If the user denies or validation fails, ?error=access_denied (or similar) is returned instead. Authorization codes are single-use and expire after 5 minutes.
const crypto = require("crypto");const codeVerifier = crypto.randomBytes(32).toString("base64url");const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");console.log({ codeVerifier, codeChallenge });
Discovery / JWKS
/.well-known/openid-configurationThe OIDC Discovery document. It returns metadata such as endpoint URLs and signing algorithms. Configure your OIDC library with this origin as the issuer and everything else resolves automatically.
/.well-known/jwks.jsonThe public keys (JWK Set) for verifying ID Token signatures. Verify the RS256 signature using the key whose kid matches the ID Token header. Keys may be rotated, so fetch the JWKS as needed or implement cache expiry (Cache-Control: max-age=300).
curl https://api.4s.link/.well-known/openid-configuration
Token Endpoint
/oauth/tokenExchanges an authorization code for tokens, or refreshes tokens with a refresh token. Send the request body as application/x-www-form-urlencoded. Confidential clients authenticate with client_secret_basic (Authorization: Basic) or client_secret_post (in the body).
Parameters for grant_type=authorization_code
grant_typestringrequiredauthorization_code
codestringrequiredThe authorization code received in the callback
redirect_uristringrequiredSame value as in the authorization request
code_verifierstringrequiredThe PKCE verifier the code_challenge was derived from
Parameters for grant_type=refresh_token
grant_typestringrequiredrefresh_token
refresh_tokenstringrequiredA valid refresh token
Refresh tokens rotate: every refresh_token grant issues a new refresh token and invalidates the old one. If a revoked refresh token is reused, all tokens for that user-client pair are revoked as a safety measure. Always persist the latest refresh token.
curl -X POST https://api.4s.link/oauth/token \-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \-H "Content-Type: application/x-www-form-urlencoded" \-d "grant_type=authorization_code" \-d "code=AUTHORIZATION_CODE" \-d "redirect_uri=https://example.com/callback" \-d "code_verifier=CODE_VERIFIER"
ID Token
The token response always contains an ID Token (an RS256-signed JWT). To validate it, verify (1) the signature with the JWKS key matching the kid, (2) iss equals the issuer, (3) aud equals your client_id, (4) exp is in the future, and (5) the nonce claim matches if you sent one. OIDC libraries handle all of this automatically.
Main claims
| Claim | Description |
|---|---|
sub | Unique user ID (the 4S user ID) |
iss | Issuer |
aud | Your client ID |
auth_time | When the user authorized (Unix seconds) |
nonce | The nonce from the authorization request (if sent) |
name / given_name / family_name / picture / profile / preferred_username / locale | Profile info (always included) |
email / email_verified | Email address (always included) |
{"sub": "a1b2c3d4-....","iss": "https://api.4s.link","aud": "YOUR_CLIENT_ID","iat": 1783900000,"exp": 1783903600,"auth_time": 1783899990,"nonce": "RANDOM_NONCE","name": "太郎 山田","given_name": "太郎","family_name": "山田","picture": "https://.../avatar.png","profile": "https://4s.link/taro","preferred_username": "taro","locale": "ja","email": "taro@example.com","email_verified": true}
Token Revocation
/oauth/revokeRFC 7009 compliant token revocation endpoint. Revoke tokens that are no longer needed, e.g. when the user signs out of your app. Revoking a refresh token also revokes the access tokens issued from it. Per RFC 7009, unknown tokens still return 200.
Body parameters
tokenstringrequiredThe access token or refresh token to revoke
curl -X POST https://api.4s.link/oauth/revoke \-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \-H "Content-Type: application/x-www-form-urlencoded" \-d "token=4s_rt_..."
Calling the APIs
Call the 4S APIs by sending your access token in the Authorization: Bearer header. OAuth access tokens can only call the read (GET) endpoints listed in the categories below.
Conventions
| Item | Detail |
|---|---|
| Base URL | https://api.4s.link |
| Auth | Authorization: Bearer <access_token> |
| Allowed scope | Only the GET endpoints listed in each category |
| Pagination | List endpoints take page / perPage query params; responses include pageInfo (totalCount / totalPages / page / perPage) |
| Error (401) | Token invalid, revoked or expired |
| Error (403) | Access to an endpoint outside the allowed GET list |
Representative endpoints for each category are listed below. For detailed request/response fields, refer to the OpenAPI specification (openapi.yaml).
curl https://api.4s.link/users/me \-H "Authorization: Bearer 4s_at_..."
Error Reference
The token endpoint, UserInfo and events APIs return RFC 6749 / RFC 6750 style errors ({ error, error_description }). Authorization endpoint errors are returned as query parameters on the redirect_uri (unless the redirect_uri itself is invalid).
| Error | Where | Description |
|---|---|---|
invalid_client | token / revoke | Client authentication failed, or the app is suspended |
invalid_grant | token | Invalid, expired or already-used code / refresh token, PKCE verification failure, redirect_uri mismatch, or consent revoked |
invalid_request | token / authorize | Missing or malformed required parameters |
access_denied | authorize | The user denied the authorization request |
interaction_required | authorize | prompt=none was requested but interaction is required (silent auth is not supported) |
unsupported_grant_type | token | A grant_type other than authorization_code / refresh_token |
401 Unauthorized | Resource APIs | The access token is invalid, revoked or expired, or the app is suspended |
403 Forbidden | Resource APIs | Access to an endpoint outside the allowed GET list (only the endpoints listed in each category are usable) |
{"error": "invalid_grant","error_description": "PKCE verification failed"}
Integration Examples
With generic OIDC libraries, setting the issuer is enough — endpoints resolve automatically via Discovery. A NextAuth (Auth.js) v5 configuration example is shown on the right. Don't forget to register your redirect_uri (e.g. https://your-app.com/api/auth/callback/4s) in the Developer menu.
Known limitations: prompt=none (silent authentication) and Dynamic Client Registration are not supported. ID Tokens do not include the at_hash claim.
import NextAuth from "next-auth";export const { handlers, auth, signIn, signOut } = NextAuth({providers: [{id: "4s",name: "4S",type: "oidc",issuer: "https://api.4s.link",clientId: process.env.FOURS_CLIENT_ID,clientSecret: process.env.FOURS_CLIENT_SECRET,authorization: {params: { scope: "openid profile email" },},// PKCE と state は NextAuth がデフォルトで有効化します},],});
Demo App
A working "Sign in with 4S" sample built with Next.js + TypeScript. See how the login flow (Authorization Code + PKCE) and profile retrieval are implemented.
Users / Profile
- GET
/usersList / search usersSearch and list users. `search` is AND-matched by whitespace across slug, name, title and bio. - GET
/users/meOwn profileRetrieve the authenticated user's own profile, including private fields such as email. - GET
/users/:idUser detailRetrieve a user's public profile (email not included). - GET
/users/:id/eventsParticipated eventsRetrieve events the user participated in (confirmed / checked in), ordered by start date descending. Private events are excluded. - GET
/users/:id/communitiesCommunitiesRetrieve communities the user belongs to (approved, non-private). No pagination. - GET
/events/:eventId/entriesOwn entry stateRetrieve the authenticated user's entries (non-canceled) for the specified event.
Organizations
Communities
- GET
/communitiesList / search communitiesSearch and list public communities. `tags` is AND-matched across all tags. - GET
/communities/:idCommunity detailRetrieve a community's detail. Returns 404 for private communities you cannot access. - GET
/communities/:id/playlistsCommunity playlistsRetrieve a community's public playlists (items not included).
Events
- GET
/eventsList / search eventsSearch and list events with rich filters. The public response omits startTime / endTime / onlineUrl (read schedules instead). groupByDate=true returns date-grouped results. - GET
/events/my-entriesOwn participated eventsRetrieve events the authenticated user has registered for, ordered by entry date descending. - GET
/events/:idEvent detailRetrieve event detail. The path accepts an ID or a slug. Includes sessions, speakers, stages, tickets and organizers. - GET
/events/:eventId/participantsParticipantsRetrieve event participants (confirmed / checked in and accepting meetings). Access is controlled by the participant list visibility setting. - GET
/events/:eventId/referral-rankingReferral influence rankingRetrieve the influence ranking (top 100) based on the event's invitation chain. - GET
/events/:eventId/entries/:entryId/referral-ancestorsReferral ancestors (inviter chain)Walk the inviter chain upward from the given entry, returned nearest-first. Returns 403 unless the caller is a confirmed participant (CONFIRMED / CHECKED_IN) of the event. - GET
/events/:eventId/entries/:entryId/referral-descendantsReferral descendants (invite tree)Expand the invite tree downward from the given entry as root. Each node's direct children are paginated by childrenLimit + cursor and include a total child count (CONFIRMED / CHECKED_IN only). Returns 403 unless the caller is a confirmed participant.
Bookmarks
Playlists
- GET
/playlistsPlaylist listSearch and list public playlists. - GET
/playlists/:playlistIdPlaylist detailRetrieve a playlist's detail (items via a separate endpoint). - GET
/playlists/:playlistId/itemsPlaylist itemsRetrieve playlist items. `page` uses numbered pagination, `cursor` uses cursor pagination. groupByDate=true returns date-grouped results.