From 24870c1f013e1217acfe12030c7602176a6fb5d6 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Wed, 11 Feb 2026 16:37:40 +0530 Subject: [PATCH 01/18] docs: add opaque API tokens documentation Add comprehensive documentation for opaque API tokens under "Add auth to your APIs" section. Covers create, validate, list, and invalidate operations with code examples in all 4 SDKs (Node.js, Python, Go, Java). Includes middleware patterns for protecting API endpoints and best practices. Co-Authored-By: Claude Opus 4.6 --- src/configs/sidebar.config.ts | 1 + .../docs/authenticate/m2m/opaque-tokens.mdx | 626 ++++++++++++++++++ 2 files changed, 627 insertions(+) create mode 100644 src/content/docs/authenticate/m2m/opaque-tokens.mdx diff --git a/src/configs/sidebar.config.ts b/src/configs/sidebar.config.ts index c967a9a28..d7c88aa91 100644 --- a/src/configs/sidebar.config.ts +++ b/src/configs/sidebar.config.ts @@ -67,6 +67,7 @@ export const sidebar = [ items: [ // 'guides/m2m/overview', TODO: It uses M2M context, and older context. Hiding it until we are sure to open it up 'authenticate/m2m/api-auth-quickstart', + 'authenticate/m2m/opaque-tokens', 'guides/m2m/scopes', // 'guides/m2m/api-auth-m2m-clients', TODO: Translate this as guides for future ], diff --git a/src/content/docs/authenticate/m2m/opaque-tokens.mdx b/src/content/docs/authenticate/m2m/opaque-tokens.mdx new file mode 100644 index 000000000..e90b5f267 --- /dev/null +++ b/src/content/docs/authenticate/m2m/opaque-tokens.mdx @@ -0,0 +1,626 @@ +--- +title: Opaque API tokens +description: "Issue long-lived, revocable API tokens scoped to organizations and users for programmatic access to your APIs" +sidebar: + label: "Opaque tokens" +head: + - tag: style + content: | + .sl-markdown-content h2 { + font-size: var(--sl-text-xl); + } + .sl-markdown-content h3 { + font-size: var(--sl-text-lg); + } +--- + +import { Aside, Steps, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components'; +import InstallSDK from '@components/templates/_installsdk.mdx'; + +Opaque API tokens are long-lived bearer credentials that grant programmatic access to your application's APIs. Each token is scoped to an organization and optionally to a specific user within that organization. Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), opaque tokens are simple strings with no embedded claims — all validation happens server-side through Scalekit's API. + +This server-side validation model provides two key security advantages. First, tokens can be **validated immediately** after creation — there is no propagation delay. Second, tokens can be **revoked instantly** — once you call invalidate, the very next validation request will reject the token. With JWTs, you must wait for the token to expire or maintain a revocation list. Opaque tokens give you real-time control over access. + +While opaque tokens are commonly used for machine-to-machine access, they also support **user-level authentication**. By scoping a token to a specific user within an organization, you can issue personal API keys that carry the user's identity. Your API middleware can then extract the `userId` from the validated token to enforce user-specific authorization — enabling use cases like personal access tokens, per-user rate limiting, and audit trails tied to individual users. + +Use opaque tokens when your customers need persistent API keys for integrations, CI/CD pipelines, service accounts, or any automation that calls your APIs. Use M2M client credentials when you need short-lived JWTs with scopes and audience restrictions. + + + +## Install the SDK + + + +Initialize the Scalekit client with your environment credentials: + + + + +```javascript title="Express.js" +import { ScalekitClient } from '@scalekit-sdk/node'; + +const scalekit = new ScalekitClient( + process.env.SCALEKIT_ENVIRONMENT_URL, + process.env.SCALEKIT_CLIENT_ID, + process.env.SCALEKIT_CLIENT_SECRET +); +``` + + + + +```python title="Flask" +from scalekit import ScalekitClient + +scalekit_client = ScalekitClient( + env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], + client_id=os.environ["SCALEKIT_CLIENT_ID"], + client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], +) +``` + + + + +```go title="Gin" +import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2" + +scalekitClient := scalekit.NewScalekitClient( + os.Getenv("SCALEKIT_ENVIRONMENT_URL"), + os.Getenv("SCALEKIT_CLIENT_ID"), + os.Getenv("SCALEKIT_CLIENT_SECRET"), +) +``` + + + + +```java title="Spring Boot" +import com.scalekit.ScalekitClient; + +ScalekitClient scalekitClient = new ScalekitClient( + System.getenv("SCALEKIT_ENVIRONMENT_URL"), + System.getenv("SCALEKIT_CLIENT_ID"), + System.getenv("SCALEKIT_CLIENT_SECRET") +); +``` + + + + +## Create a token + +Create an opaque token scoped to an organization. You can optionally scope it to a specific user, attach custom claims as key-value metadata, set an expiry, and add a human-readable description. + + + + +```javascript title="Express.js" +// Create a basic organization-scoped token +const response = await scalekit.token.createToken(organizationId, { + description: 'CI/CD pipeline token', +}); + +// The opaque token string — store this securely +const opaqueToken = response.token; +// The token identifier (format: apit_xxxxx) +const tokenId = response.tokenId; + +// Create a user-scoped token with custom claims +const userToken = await scalekit.token.createToken(organizationId, { + userId: 'usr_12345', + customClaims: { + team: 'engineering', + environment: 'production', + }, + description: 'Deployment service token', +}); +``` + + + + +```python title="Flask" +# Create a basic organization-scoped token +response = scalekit_client.tokens.create_token( + organization_id=organization_id, + description="CI/CD pipeline token", +) + +# The opaque token string — store this securely +opaque_token = response[0].token +# The token identifier (format: apit_xxxxx) +token_id = response[0].token_id + +# Create a user-scoped token with custom claims +user_token = scalekit_client.tokens.create_token( + organization_id=organization_id, + user_id="usr_12345", + custom_claims={ + "team": "engineering", + "environment": "production", + }, + description="Deployment service token", +) +``` + + + + +```go title="Gin" +// Create a basic organization-scoped token +response, err := scalekitClient.Token().CreateToken( + ctx, organizationId, scalekit.CreateTokenOptions{ + Description: "CI/CD pipeline token", + }, +) + +// The opaque token string — store this securely +opaqueToken := response.Token +// The token identifier (format: apit_xxxxx) +tokenId := response.TokenId + +// Create a user-scoped token with custom claims +userToken, err := scalekitClient.Token().CreateToken( + ctx, organizationId, scalekit.CreateTokenOptions{ + UserId: "usr_12345", + CustomClaims: map[string]string{ + "team": "engineering", + "environment": "production", + }, + Description: "Deployment service token", + }, +) +``` + + + + +```java title="Spring Boot" +// Create a basic organization-scoped token +CreateTokenResponse response = scalekitClient.tokens().create(organizationId); + +// The opaque token string — store this securely +String opaqueToken = response.getToken(); +// The token identifier (format: apit_xxxxx) +String tokenId = response.getTokenId(); + +// Create a user-scoped token with custom claims +Map customClaims = Map.of( + "team", "engineering", + "environment", "production" +); + +CreateTokenResponse userToken = scalekitClient.tokens().create( + organizationId, "usr_12345", customClaims, null, "Deployment service token" +); +``` + + + + +The response contains three fields: + +| Field | Description | +|-------|-------------| +| `token` | The opaque token string. **Returned only at creation.** | +| `token_id` | A stable identifier (format: `apit_xxxxx`) for referencing the token in management operations. | +| `token_info` | Metadata including organization, user, custom claims, and timestamps. | + +## Validate a token + +Validate an incoming token to verify it is active and retrieve its associated context. You can pass either the opaque token string or the `token_id`. + + + + +```javascript title="Express.js" +// Validate using the opaque token from the Authorization header +const result = await scalekit.token.validateToken(opaqueToken); + +// Access the token context +const orgId = result.tokenInfo.organizationId; +const userId = result.tokenInfo.userId; +const claims = result.tokenInfo.customClaims; + +// You can also validate by token_id +const resultById = await scalekit.token.validateToken(tokenId); +``` + + + + +```python title="Flask" +# Validate using the opaque token from the Authorization header +result = scalekit_client.tokens.validate_token(token=opaque_token) + +# Access the token context +org_id = result[0].token_info.organization_id +user_id = result[0].token_info.user_id +claims = result[0].token_info.custom_claims + +# You can also validate by token_id +result_by_id = scalekit_client.tokens.validate_token(token=token_id) +``` + + + + +```go title="Gin" +// Validate using the opaque token from the Authorization header +result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) + +// Access the token context +orgId := result.TokenInfo.OrganizationId +userId := result.TokenInfo.UserId +claims := result.TokenInfo.CustomClaims + +// You can also validate by token_id +resultById, err := scalekitClient.Token().ValidateToken(ctx, tokenId) +``` + + + + +```java title="Spring Boot" +// Validate using the opaque token from the Authorization header +ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + +// Access the token context +String orgId = result.getTokenInfo().getOrganizationId(); +String userId = result.getTokenInfo().getUserId(); +Map claims = result.getTokenInfo().getCustomClaimsMap(); + +// You can also validate by token_id +ValidateTokenResponse resultById = scalekitClient.tokens().validate(tokenId); +``` + + + + +Validation fails with an error if the token is invalid, expired, or has been revoked. Use this behavior to reject unauthorized requests in your API middleware. + +## List tokens + +Retrieve all active tokens for an organization. Use pagination to handle large result sets, and filter by `userId` to find tokens scoped to a specific user. + + + + +```javascript title="Express.js" +// List tokens for an organization +const response = await scalekit.token.listTokens(organizationId, { + pageSize: 10, +}); + +for (const token of response.tokens) { + console.log(token.tokenId, token.description); +} + +// Paginate through results +if (response.nextPageToken) { + const nextPage = await scalekit.token.listTokens(organizationId, { + pageSize: 10, + pageToken: response.nextPageToken, + }); +} + +// Filter tokens by user +const userTokens = await scalekit.token.listTokens(organizationId, { + userId: 'usr_12345', +}); +``` + + + + +```python title="Flask" +# List tokens for an organization +response = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + page_size=10, +) + +for token in response[0].tokens: + print(token.token_id, token.description) + +# Paginate through results +if response[0].next_page_token: + next_page = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + page_size=10, + page_token=response[0].next_page_token, + ) + +# Filter tokens by user +user_tokens = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + user_id="usr_12345", +) +``` + + + + +```go title="Gin" +// List tokens for an organization +response, err := scalekitClient.Token().ListTokens( + ctx, organizationId, scalekit.ListTokensOptions{ + PageSize: 10, + }, +) + +for _, token := range response.Tokens { + fmt.Println(token.TokenId, token.GetDescription()) +} + +// Paginate through results +if response.NextPageToken != "" { + nextPage, err := scalekitClient.Token().ListTokens( + ctx, organizationId, scalekit.ListTokensOptions{ + PageSize: 10, + PageToken: response.NextPageToken, + }, + ) +} + +// Filter tokens by user +userTokens, err := scalekitClient.Token().ListTokens( + ctx, organizationId, scalekit.ListTokensOptions{ + UserId: "usr_12345", + }, +) +``` + + + + +```java title="Spring Boot" +// List tokens for an organization +ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); + +for (Token token : response.getTokensList()) { + System.out.println(token.getTokenId() + " " + token.getDescription()); +} + +// Paginate through results +if (!response.getNextPageToken().isEmpty()) { + ListTokensResponse nextPage = scalekitClient.tokens().list( + organizationId, 10, response.getNextPageToken() + ); +} + +// Filter tokens by user +ListTokensResponse userTokens = scalekitClient.tokens().list( + organizationId, "usr_12345", 10, null +); +``` + + + + +The response includes `totalCount` for the total number of matching tokens and `nextPageToken` / `prevPageToken` cursors for navigating pages. + +## Invalidate a token + +Revoke a token to immediately prevent it from being used. Invalidation takes effect instantly — any subsequent validation request for this token will fail. + +This operation is **idempotent**: calling invalidate on an already-revoked token succeeds without error. + + + + +```javascript title="Express.js" +// Invalidate by token_id +await scalekit.token.invalidateToken(tokenId); + +// Or invalidate by opaque token string +await scalekit.token.invalidateToken(opaqueToken); +``` + + + + +```python title="Flask" +# Invalidate by token_id +scalekit_client.tokens.invalidate_token(token=token_id) + +# Or invalidate by opaque token string +scalekit_client.tokens.invalidate_token(token=opaque_token) +``` + + + + +```go title="Gin" +// Invalidate by token_id +err := scalekitClient.Token().InvalidateToken(ctx, tokenId) + +// Or invalidate by opaque token string +err = scalekitClient.Token().InvalidateToken(ctx, opaqueToken) +``` + + + + +```java title="Spring Boot" +// Invalidate by token_id +scalekitClient.tokens().invalidate(tokenId); + +// Or invalidate by opaque token string +scalekitClient.tokens().invalidate(opaqueToken); +``` + + + + +## Protect your API endpoints + +Add token validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned `token_info` for authorization decisions. + + + + +```javascript title="Express.js" +async function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Missing authorization token' }); + } + + try { + const result = await scalekit.token.validateToken(token); + // Attach token context to the request for downstream handlers + req.tokenInfo = result.tokenInfo; + next(); + } catch (error) { + return res.status(401).json({ error: 'Invalid or expired token' }); + } +} + +// Apply to protected routes +app.get('/api/resources', authenticateToken, (req, res) => { + const orgId = req.tokenInfo.organizationId; + // Serve resources scoped to this organization +}); +``` + + + + +```python title="Flask" +from functools import wraps +from flask import request, jsonify, g + +def authenticate_token(f): + @wraps(f) + def decorated(*args, **kwargs): + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + return jsonify({"error": "Missing authorization token"}), 401 + + token = auth_header.split(" ")[1] + + try: + result = scalekit_client.tokens.validate_token(token=token) + # Attach token context for downstream handlers + g.token_info = result[0].token_info + except Exception: + return jsonify({"error": "Invalid or expired token"}), 401 + + return f(*args, **kwargs) + return decorated + +# Apply to protected routes +@app.route("/api/resources") +@authenticate_token +def get_resources(): + org_id = g.token_info.organization_id + # Serve resources scoped to this organization +``` + + + + +```go title="Gin" +func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(401, gin.H{"error": "Missing authorization token"}) + c.Abort() + return + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + + result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token) + if err != nil { + c.JSON(401, gin.H{"error": "Invalid or expired token"}) + c.Abort() + return + } + + // Attach token context for downstream handlers + c.Set("tokenInfo", result.TokenInfo) + c.Next() + } +} + +// Apply to protected routes +r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) { + tokenInfo := c.MustGet("tokenInfo").(*scalekit.TokenInfo) + orgId := tokenInfo.OrganizationId + // Serve resources scoped to this organization +}) +``` + + + + +```java title="Spring Boot" +@Component +public class TokenAuthFilter extends OncePerRequestFilter { + private final ScalekitClient scalekitClient; + + public TokenAuthFilter(ScalekitClient scalekitClient) { + this.scalekitClient = scalekitClient; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + response.sendError(401, "Missing authorization token"); + return; + } + + String token = authHeader.substring(7); + + try { + ValidateTokenResponse result = scalekitClient.tokens().validate(token); + // Attach token context for downstream handlers + request.setAttribute("tokenInfo", result.getTokenInfo()); + filterChain.doFilter(request, response); + } catch (Exception e) { + response.sendError(401, "Invalid or expired token"); + } + } +} + +// Access in your controller +@GetMapping("/api/resources") +public ResponseEntity getResources(HttpServletRequest request) { + Token tokenInfo = (Token) request.getAttribute("tokenInfo"); + String orgId = tokenInfo.getOrganizationId(); + // Serve resources scoped to this organization +} +``` + + + + +## Best practices + + + + Treat opaque tokens like passwords. Store them in encrypted secrets managers or environment variables. Never log tokens, commit them to version control, or expose them in client-side code. + + + Use the `expiry` parameter for tokens that should automatically become invalid after a set period. This limits the blast radius if a token is compromised. + + + Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups. + + + To rotate a token: create a new token, update the consuming service to use the new token, verify the new token works, then invalidate the old token. This avoids downtime during rotation. + + From 97ce579791634b3b07c63756157ced3528e5471c Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Wed, 11 Feb 2026 18:09:27 +0530 Subject: [PATCH 02/18] docs: improve opaque tokens page per writing standards review Add tags, error handling, security comments, collapse for init blocks, Python tuple explanation, and closing next-steps section. --- .../docs/authenticate/m2m/opaque-tokens.mdx | 233 +++++++++++------- 1 file changed, 144 insertions(+), 89 deletions(-) diff --git a/src/content/docs/authenticate/m2m/opaque-tokens.mdx b/src/content/docs/authenticate/m2m/opaque-tokens.mdx index e90b5f267..8944a5fbd 100644 --- a/src/content/docs/authenticate/m2m/opaque-tokens.mdx +++ b/src/content/docs/authenticate/m2m/opaque-tokens.mdx @@ -3,6 +3,7 @@ title: Opaque API tokens description: "Issue long-lived, revocable API tokens scoped to organizations and users for programmatic access to your APIs" sidebar: label: "Opaque tokens" +tags: [api-tokens, opaque-tokens, m2m, authentication, bearer-tokens] head: - tag: style content: | @@ -38,7 +39,7 @@ Initialize the Scalekit client with your environment credentials: -```javascript title="Express.js" +```javascript title="Express.js" collapse={1-2} import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( @@ -51,7 +52,8 @@ const scalekit = new ScalekitClient( -```python title="Flask" +```python title="Flask" collapse={1-2} +import os from scalekit import ScalekitClient scalekit_client = ScalekitClient( @@ -64,7 +66,7 @@ scalekit_client = ScalekitClient( -```go title="Gin" +```go title="Gin" collapse={1-2} import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2" scalekitClient := scalekit.NewScalekitClient( @@ -77,7 +79,7 @@ scalekitClient := scalekit.NewScalekitClient( -```java title="Spring Boot" +```java title="Spring Boot" collapse={1-2} import com.scalekit.ScalekitClient; ScalekitClient scalekitClient = new ScalekitClient( @@ -98,52 +100,58 @@ Create an opaque token scoped to an organization. You can optionally scope it to ```javascript title="Express.js" -// Create a basic organization-scoped token -const response = await scalekit.token.createToken(organizationId, { - description: 'CI/CD pipeline token', -}); - -// The opaque token string — store this securely -const opaqueToken = response.token; -// The token identifier (format: apit_xxxxx) -const tokenId = response.tokenId; +try { + // Create a basic organization-scoped token + const response = await scalekit.token.createToken(organizationId, { + description: 'CI/CD pipeline token', + }); -// Create a user-scoped token with custom claims -const userToken = await scalekit.token.createToken(organizationId, { - userId: 'usr_12345', - customClaims: { - team: 'engineering', - environment: 'production', - }, - description: 'Deployment service token', -}); + // Store securely — this value cannot be retrieved again after creation + const opaqueToken = response.token; + // Stable identifier for management operations (format: apit_xxxxx) + const tokenId = response.tokenId; + + // Create a user-scoped token with custom claims + const userToken = await scalekit.token.createToken(organizationId, { + userId: 'usr_12345', + customClaims: { + team: 'engineering', + environment: 'production', + }, + description: 'Deployment service token', + }); +} catch (error) { + console.error('Failed to create token:', error.message); +} ``` ```python title="Flask" -# Create a basic organization-scoped token -response = scalekit_client.tokens.create_token( - organization_id=organization_id, - description="CI/CD pipeline token", -) +try: + # Create a basic organization-scoped token + response = scalekit_client.tokens.create_token( + organization_id=organization_id, + description="CI/CD pipeline token", + ) -# The opaque token string — store this securely -opaque_token = response[0].token -# The token identifier (format: apit_xxxxx) -token_id = response[0].token_id + # SDK returns (response, metadata) tuple — access response at index 0 + opaque_token = response[0].token # store this securely + token_id = response[0].token_id # format: apit_xxxxx -# Create a user-scoped token with custom claims -user_token = scalekit_client.tokens.create_token( - organization_id=organization_id, - user_id="usr_12345", - custom_claims={ - "team": "engineering", - "environment": "production", - }, - description="Deployment service token", -) + # Create a user-scoped token with custom claims + user_token = scalekit_client.tokens.create_token( + organization_id=organization_id, + user_id="usr_12345", + custom_claims={ + "team": "engineering", + "environment": "production", + }, + description="Deployment service token", + ) +except Exception as e: + print(f"Failed to create token: {e}") ``` @@ -156,10 +164,13 @@ response, err := scalekitClient.Token().CreateToken( Description: "CI/CD pipeline token", }, ) +if err != nil { + log.Fatalf("Failed to create token: %v", err) +} -// The opaque token string — store this securely +// Store securely — this value cannot be retrieved again after creation opaqueToken := response.Token -// The token identifier (format: apit_xxxxx) +// Stable identifier for management operations (format: apit_xxxxx) tokenId := response.TokenId // Create a user-scoped token with custom claims @@ -173,29 +184,36 @@ userToken, err := scalekitClient.Token().CreateToken( Description: "Deployment service token", }, ) +if err != nil { + log.Fatalf("Failed to create user token: %v", err) +} ``` ```java title="Spring Boot" -// Create a basic organization-scoped token -CreateTokenResponse response = scalekitClient.tokens().create(organizationId); - -// The opaque token string — store this securely -String opaqueToken = response.getToken(); -// The token identifier (format: apit_xxxxx) -String tokenId = response.getTokenId(); - -// Create a user-scoped token with custom claims -Map customClaims = Map.of( - "team", "engineering", - "environment", "production" -); +try { + // Create a basic organization-scoped token + CreateTokenResponse response = scalekitClient.tokens().create(organizationId); + + // Store securely — this value cannot be retrieved again after creation + String opaqueToken = response.getToken(); + // Stable identifier for management operations (format: apit_xxxxx) + String tokenId = response.getTokenId(); + + // Create a user-scoped token with custom claims + Map customClaims = Map.of( + "team", "engineering", + "environment", "production" + ); -CreateTokenResponse userToken = scalekitClient.tokens().create( - organizationId, "usr_12345", customClaims, null, "Deployment service token" -); + CreateTokenResponse userToken = scalekitClient.tokens().create( + organizationId, "usr_12345", customClaims, null, "Deployment service token" + ); +} catch (Exception e) { + System.err.println("Failed to create token: " + e.getMessage()); +} ``` @@ -217,32 +235,41 @@ Validate an incoming token to verify it is active and retrieve its associated co ```javascript title="Express.js" -// Validate using the opaque token from the Authorization header -const result = await scalekit.token.validateToken(opaqueToken); - -// Access the token context -const orgId = result.tokenInfo.organizationId; -const userId = result.tokenInfo.userId; -const claims = result.tokenInfo.customClaims; - -// You can also validate by token_id -const resultById = await scalekit.token.validateToken(tokenId); +try { + // Validate using the opaque token from the Authorization header + const result = await scalekit.token.validateToken(opaqueToken); + + // Access the token context + const orgId = result.tokenInfo.organizationId; + const userId = result.tokenInfo.userId; + const claims = result.tokenInfo.customClaims; + + // You can also validate by token_id + const resultById = await scalekit.token.validateToken(tokenId); +} catch (error) { + // Token is invalid, expired, or revoked + console.error('Token validation failed:', error.message); +} ``` ```python title="Flask" -# Validate using the opaque token from the Authorization header -result = scalekit_client.tokens.validate_token(token=opaque_token) - -# Access the token context -org_id = result[0].token_info.organization_id -user_id = result[0].token_info.user_id -claims = result[0].token_info.custom_claims - -# You can also validate by token_id -result_by_id = scalekit_client.tokens.validate_token(token=token_id) +try: + # Validate using the opaque token from the Authorization header + result = scalekit_client.tokens.validate_token(token=opaque_token) + + # Access the token context + org_id = result[0].token_info.organization_id + user_id = result[0].token_info.user_id + claims = result[0].token_info.custom_claims + + # You can also validate by token_id + result_by_id = scalekit_client.tokens.validate_token(token=token_id) +except Exception: + # Token is invalid, expired, or revoked + print("Token validation failed") ``` @@ -251,6 +278,10 @@ result_by_id = scalekit_client.tokens.validate_token(token=token_id) ```go title="Gin" // Validate using the opaque token from the Authorization header result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) +if err != nil { + // Token is invalid, expired, or revoked + log.Fatalf("Token validation failed: %v", err) +} // Access the token context orgId := result.TokenInfo.OrganizationId @@ -259,22 +290,30 @@ claims := result.TokenInfo.CustomClaims // You can also validate by token_id resultById, err := scalekitClient.Token().ValidateToken(ctx, tokenId) +if err != nil { + log.Fatalf("Token validation failed: %v", err) +} ``` ```java title="Spring Boot" -// Validate using the opaque token from the Authorization header -ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); - -// Access the token context -String orgId = result.getTokenInfo().getOrganizationId(); -String userId = result.getTokenInfo().getUserId(); -Map claims = result.getTokenInfo().getCustomClaimsMap(); - -// You can also validate by token_id -ValidateTokenResponse resultById = scalekitClient.tokens().validate(tokenId); +try { + // Validate using the opaque token from the Authorization header + ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + + // Access the token context + String orgId = result.getTokenInfo().getOrganizationId(); + String userId = result.getTokenInfo().getUserId(); + Map claims = result.getTokenInfo().getCustomClaimsMap(); + + // You can also validate by token_id + ValidateTokenResponse resultById = scalekitClient.tokens().validate(tokenId); +} catch (Exception e) { + // Token is invalid, expired, or revoked + System.err.println("Token validation failed: " + e.getMessage()); +} ``` @@ -469,15 +508,18 @@ async function authenticateToken(req, res, next) { const token = authHeader && authHeader.split(' ')[1]; if (!token) { + // Reject requests without credentials to prevent unauthorized access return res.status(401).json({ error: 'Missing authorization token' }); } try { + // Server-side validation — Scalekit checks token status in real time const result = await scalekit.token.validateToken(token); // Attach token context to the request for downstream handlers req.tokenInfo = result.tokenInfo; next(); } catch (error) { + // Revoked, expired, or malformed tokens are rejected immediately return res.status(401).json({ error: 'Invalid or expired token' }); } } @@ -501,15 +543,18 @@ def authenticate_token(f): def decorated(*args, **kwargs): auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): + # Reject requests without credentials to prevent unauthorized access return jsonify({"error": "Missing authorization token"}), 401 token = auth_header.split(" ")[1] try: + # Server-side validation — Scalekit checks token status in real time result = scalekit_client.tokens.validate_token(token=token) # Attach token context for downstream handlers g.token_info = result[0].token_info except Exception: + # Revoked, expired, or malformed tokens are rejected immediately return jsonify({"error": "Invalid or expired token"}), 401 return f(*args, **kwargs) @@ -531,6 +576,7 @@ func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { + // Reject requests without credentials to prevent unauthorized access c.JSON(401, gin.H{"error": "Missing authorization token"}) c.Abort() return @@ -538,8 +584,10 @@ func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc { token := strings.TrimPrefix(authHeader, "Bearer ") + // Server-side validation — Scalekit checks token status in real time result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token) if err != nil { + // Revoked, expired, or malformed tokens are rejected immediately c.JSON(401, gin.H{"error": "Invalid or expired token"}) c.Abort() return @@ -579,6 +627,7 @@ public class TokenAuthFilter extends OncePerRequestFilter { ) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { + // Reject requests without credentials to prevent unauthorized access response.sendError(401, "Missing authorization token"); return; } @@ -586,11 +635,13 @@ public class TokenAuthFilter extends OncePerRequestFilter { String token = authHeader.substring(7); try { + // Server-side validation — Scalekit checks token status in real time ValidateTokenResponse result = scalekitClient.tokens().validate(token); // Attach token context for downstream handlers request.setAttribute("tokenInfo", result.getTokenInfo()); filterChain.doFilter(request, response); } catch (Exception e) { + // Revoked, expired, or malformed tokens are rejected immediately response.sendError(401, "Invalid or expired token"); } } @@ -624,3 +675,7 @@ public ResponseEntity getResources(HttpServletRequest request) { To rotate a token: create a new token, update the consuming service to use the new token, verify the new token works, then invalidate the old token. This avoids downtime during rotation. + +## Next steps + +You now have the building blocks to issue, validate, and manage opaque API tokens in your application. For short-lived, scoped authentication between services, see [M2M authentication with client credentials](/authenticate/m2m/api-auth-quickstart/). To control what each token can access, configure [scopes for your M2M clients](/guides/m2m/scopes/). From 9ab4d072a39845507fe2aa988fc2587ac6433f60 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Thu, 12 Feb 2026 01:09:59 +0530 Subject: [PATCH 03/18] docs: rename opaque tokens to API keys with tone shift and structural improvements - Rename opaque-tokens.mdx to api-keys.mdx, update sidebar config - Rewrite intro with Scalekit recommendation tone - Add D2 sequence diagram showing API key lifecycle flow - Split create token into org-scoped and user-scoped subsections - Add validate subsections for roles/externalOrgId and custom metadata - Reorder invalidate examples (API key first, tokenId second) - Remove framework titles from generic code examples - Replace log.Fatalf with log.Printf in Go examples - Update all prose references from opaque tokens to API keys --- .../d2/docs/authenticate/m2m/api-keys-0.svg | 195 ++++++++ src/configs/sidebar.config.ts | 2 +- .../m2m/{opaque-tokens.mdx => api-keys.mdx} | 416 +++++++++++++----- 3 files changed, 497 insertions(+), 116 deletions(-) create mode 100644 public/d2/docs/authenticate/m2m/api-keys-0.svg rename src/content/docs/authenticate/m2m/{opaque-tokens.mdx => api-keys.mdx} (60%) diff --git a/public/d2/docs/authenticate/m2m/api-keys-0.svg b/public/d2/docs/authenticate/m2m/api-keys-0.svg new file mode 100644 index 000000000..306e7847f --- /dev/null +++ b/public/d2/docs/authenticate/m2m/api-keys-0.svg @@ -0,0 +1,195 @@ + + + + + + + + + + + + +API ClientUserYour AppScalekit Request API key Create token (organizationId, userId) API key + tokenId API key Configure API key Request with Authorization header Validate token Organization, user, and role info Process request Response + + + + + + + + + + + + diff --git a/src/configs/sidebar.config.ts b/src/configs/sidebar.config.ts index d7c88aa91..38cde1228 100644 --- a/src/configs/sidebar.config.ts +++ b/src/configs/sidebar.config.ts @@ -67,7 +67,7 @@ export const sidebar = [ items: [ // 'guides/m2m/overview', TODO: It uses M2M context, and older context. Hiding it until we are sure to open it up 'authenticate/m2m/api-auth-quickstart', - 'authenticate/m2m/opaque-tokens', + 'authenticate/m2m/api-keys', 'guides/m2m/scopes', // 'guides/m2m/api-auth-m2m-clients', TODO: Translate this as guides for future ], diff --git a/src/content/docs/authenticate/m2m/opaque-tokens.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx similarity index 60% rename from src/content/docs/authenticate/m2m/opaque-tokens.mdx rename to src/content/docs/authenticate/m2m/api-keys.mdx index 8944a5fbd..c7525f1d1 100644 --- a/src/content/docs/authenticate/m2m/opaque-tokens.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -1,9 +1,9 @@ --- -title: Opaque API tokens -description: "Issue long-lived, revocable API tokens scoped to organizations and users for programmatic access to your APIs" +title: API keys +description: "Issue long-lived, revocable API keys scoped to organizations and users for programmatic access to your APIs" sidebar: - label: "Opaque tokens" -tags: [api-tokens, opaque-tokens, m2m, authentication, bearer-tokens] + label: "API keys" +tags: [api-tokens, api-keys, m2m, authentication, bearer-tokens] head: - tag: style content: | @@ -18,16 +18,36 @@ head: import { Aside, Steps, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components'; import InstallSDK from '@components/templates/_installsdk.mdx'; -Opaque API tokens are long-lived bearer credentials that grant programmatic access to your application's APIs. Each token is scoped to an organization and optionally to a specific user within that organization. Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), opaque tokens are simple strings with no embedded claims — all validation happens server-side through Scalekit's API. +Scalekit API keys let you give your customers long-lived, revocable credentials for programmatic access to your APIs. Each API key is scoped to an organization and optionally to a specific user, so you have fine-grained control over who can access what. Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys are simple bearer strings with no embedded claims — Scalekit handles all validation server-side. -This server-side validation model provides two key security advantages. First, tokens can be **validated immediately** after creation — there is no propagation delay. Second, tokens can be **revoked instantly** — once you call invalidate, the very next validation request will reject the token. With JWTs, you must wait for the token to expire or maintain a revocation list. Opaque tokens give you real-time control over access. +Scalekit validates every API key in real time, which gives you two security advantages out of the box. Keys can be **validated immediately** after creation — there is no propagation delay. And keys can be **revoked instantly** — once you call invalidate, the very next validation request rejects the key. No waiting for expiry windows or maintaining revocation lists. -While opaque tokens are commonly used for machine-to-machine access, they also support **user-level authentication**. By scoping a token to a specific user within an organization, you can issue personal API keys that carry the user's identity. Your API middleware can then extract the `userId` from the validated token to enforce user-specific authorization — enabling use cases like personal access tokens, per-user rate limiting, and audit trails tied to individual users. +Scalekit API keys also support **user-level scoping**, which enables you to issue personal API keys tied to a specific user within an organization. Your API middleware can extract the `userId` from the validated key to enforce user-specific authorization — enabling personal access tokens, per-user rate limiting, and audit trails tied to individual users. -Use opaque tokens when your customers need persistent API keys for integrations, CI/CD pipelines, service accounts, or any automation that calls your APIs. Use M2M client credentials when you need short-lived JWTs with scopes and audience restrictions. +We recommend API keys when your customers need persistent credentials for integrations, CI/CD pipelines, service accounts, or any automation that calls your APIs. For short-lived JWTs with scopes and audience restrictions, use [M2M client credentials](/authenticate/m2m/api-auth-quickstart/) instead. + +```d2 pad=36 +shape: sequence_diagram + +API Client +User +Your App +Scalekit + +User -> Your App: Request API key +Your App -> Scalekit: Create token (organizationId, userId) +Scalekit -> Your App: API key + tokenId +Your App -> User: API key +User -> API Client: Configure API key +API Client -> Your App: Request with Authorization header +Your App -> Scalekit: Validate token +Scalekit -> Your App: Organization, user, and role info +Your App -> Your App: Process request +Your App -> API Client: Response +``` ## Install the SDK @@ -94,14 +114,15 @@ ScalekitClient scalekitClient = new ScalekitClient( ## Create a token -Create an opaque token scoped to an organization. You can optionally scope it to a specific user, attach custom claims as key-value metadata, set an expiry, and add a human-readable description. +### Organization-scoped API key + +Create an API key scoped to an organization. This is the most common pattern for service-to-service integrations where the API key represents access on behalf of an entire organization. -```javascript title="Express.js" +```javascript try { - // Create a basic organization-scoped token const response = await scalekit.token.createToken(organizationId, { description: 'CI/CD pipeline token', }); @@ -110,16 +131,6 @@ try { const opaqueToken = response.token; // Stable identifier for management operations (format: apit_xxxxx) const tokenId = response.tokenId; - - // Create a user-scoped token with custom claims - const userToken = await scalekit.token.createToken(organizationId, { - userId: 'usr_12345', - customClaims: { - team: 'engineering', - environment: 'production', - }, - description: 'Deployment service token', - }); } catch (error) { console.error('Failed to create token:', error.message); } @@ -128,9 +139,8 @@ try { -```python title="Flask" +```python try: - # Create a basic organization-scoped token response = scalekit_client.tokens.create_token( organization_id=organization_id, description="CI/CD pipeline token", @@ -139,17 +149,6 @@ try: # SDK returns (response, metadata) tuple — access response at index 0 opaque_token = response[0].token # store this securely token_id = response[0].token_id # format: apit_xxxxx - - # Create a user-scoped token with custom claims - user_token = scalekit_client.tokens.create_token( - organization_id=organization_id, - user_id="usr_12345", - custom_claims={ - "team": "engineering", - "environment": "production", - }, - description="Deployment service token", - ) except Exception as e: print(f"Failed to create token: {e}") ``` @@ -157,23 +156,92 @@ except Exception as e: -```go title="Gin" -// Create a basic organization-scoped token +```go response, err := scalekitClient.Token().CreateToken( ctx, organizationId, scalekit.CreateTokenOptions{ Description: "CI/CD pipeline token", }, ) if err != nil { - log.Fatalf("Failed to create token: %v", err) + log.Printf("Failed to create token: %v", err) + return } // Store securely — this value cannot be retrieved again after creation opaqueToken := response.Token // Stable identifier for management operations (format: apit_xxxxx) tokenId := response.TokenId +``` + + + + +```java +try { + CreateTokenResponse response = scalekitClient.tokens().create(organizationId); + + // Store securely — this value cannot be retrieved again after creation + String opaqueToken = response.getToken(); + // Stable identifier for management operations (format: apit_xxxxx) + String tokenId = response.getTokenId(); +} catch (Exception e) { + System.err.println("Failed to create token: " + e.getMessage()); +} +``` + + + + +### User-scoped API key + +Scope an API key to a specific user within an organization to enable personal access tokens, per-user audit trails, and user-level rate limiting. You can also attach custom claims as key-value metadata. + + + -// Create a user-scoped token with custom claims +```javascript +try { + const userToken = await scalekit.token.createToken(organizationId, { + userId: 'usr_12345', + customClaims: { + team: 'engineering', + environment: 'production', + }, + description: 'Deployment service token', + }); + + const opaqueToken = userToken.token; + const tokenId = userToken.tokenId; +} catch (error) { + console.error('Failed to create token:', error.message); +} +``` + + + + +```python +try: + user_token = scalekit_client.tokens.create_token( + organization_id=organization_id, + user_id="usr_12345", + custom_claims={ + "team": "engineering", + "environment": "production", + }, + description="Deployment service token", + ) + + opaque_token = user_token[0].token + token_id = user_token[0].token_id +except Exception as e: + print(f"Failed to create token: {e}") +``` + + + + +```go userToken, err := scalekitClient.Token().CreateToken( ctx, organizationId, scalekit.CreateTokenOptions{ UserId: "usr_12345", @@ -185,24 +253,19 @@ userToken, err := scalekitClient.Token().CreateToken( }, ) if err != nil { - log.Fatalf("Failed to create user token: %v", err) + log.Printf("Failed to create user token: %v", err) + return } + +opaqueToken := userToken.Token +tokenId := userToken.TokenId ``` -```java title="Spring Boot" +```java try { - // Create a basic organization-scoped token - CreateTokenResponse response = scalekitClient.tokens().create(organizationId); - - // Store securely — this value cannot be retrieved again after creation - String opaqueToken = response.getToken(); - // Stable identifier for management operations (format: apit_xxxxx) - String tokenId = response.getTokenId(); - - // Create a user-scoped token with custom claims Map customClaims = Map.of( "team", "engineering", "environment", "production" @@ -211,6 +274,9 @@ try { CreateTokenResponse userToken = scalekitClient.tokens().create( organizationId, "usr_12345", customClaims, null, "Deployment service token" ); + + String opaqueToken = userToken.getToken(); + String tokenId = userToken.getTokenId(); } catch (Exception e) { System.err.println("Failed to create token: " + e.getMessage()); } @@ -223,29 +289,24 @@ The response contains three fields: | Field | Description | |-------|-------------| -| `token` | The opaque token string. **Returned only at creation.** | +| `token` | The API key string. **Returned only at creation.** | | `token_id` | A stable identifier (format: `apit_xxxxx`) for referencing the token in management operations. | | `token_info` | Metadata including organization, user, custom claims, and timestamps. | ## Validate a token -Validate an incoming token to verify it is active and retrieve its associated context. You can pass either the opaque token string or the `token_id`. +Scalekit validates the API key server-side and returns its associated context. -```javascript title="Express.js" +```javascript try { - // Validate using the opaque token from the Authorization header const result = await scalekit.token.validateToken(opaqueToken); - // Access the token context const orgId = result.tokenInfo.organizationId; const userId = result.tokenInfo.userId; const claims = result.tokenInfo.customClaims; - - // You can also validate by token_id - const resultById = await scalekit.token.validateToken(tokenId); } catch (error) { // Token is invalid, expired, or revoked console.error('Token validation failed:', error.message); @@ -255,18 +316,13 @@ try { -```python title="Flask" +```python try: - # Validate using the opaque token from the Authorization header result = scalekit_client.tokens.validate_token(token=opaque_token) - # Access the token context org_id = result[0].token_info.organization_id user_id = result[0].token_info.user_id claims = result[0].token_info.custom_claims - - # You can also validate by token_id - result_by_id = scalekit_client.tokens.validate_token(token=token_id) except Exception: # Token is invalid, expired, or revoked print("Token validation failed") @@ -275,41 +331,29 @@ except Exception: -```go title="Gin" -// Validate using the opaque token from the Authorization header +```go result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) if err != nil { // Token is invalid, expired, or revoked - log.Fatalf("Token validation failed: %v", err) + log.Printf("Token validation failed: %v", err) + return } -// Access the token context orgId := result.TokenInfo.OrganizationId userId := result.TokenInfo.UserId claims := result.TokenInfo.CustomClaims - -// You can also validate by token_id -resultById, err := scalekitClient.Token().ValidateToken(ctx, tokenId) -if err != nil { - log.Fatalf("Token validation failed: %v", err) -} ``` -```java title="Spring Boot" +```java try { - // Validate using the opaque token from the Authorization header ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); - // Access the token context String orgId = result.getTokenInfo().getOrganizationId(); String userId = result.getTokenInfo().getUserId(); Map claims = result.getTokenInfo().getCustomClaimsMap(); - - // You can also validate by token_id - ValidateTokenResponse resultById = scalekitClient.tokens().validate(tokenId); } catch (Exception e) { // Token is invalid, expired, or revoked System.err.println("Token validation failed: " + e.getMessage()); @@ -319,16 +363,158 @@ try { -Validation fails with an error if the token is invalid, expired, or has been revoked. Use this behavior to reject unauthorized requests in your API middleware. +Validation fails with an error if the API key is invalid, expired, or has been revoked. Use this behavior to reject unauthorized requests in your API middleware. + +### Access roles and organization details + +The validated token also includes roles assigned to the user and external identifiers for the organization. Use these to make authorization decisions without additional lookups. + + + + +```javascript +const result = await scalekit.token.validateToken(opaqueToken); + +// Roles assigned to the user +const roles = result.tokenInfo.roles; + +// External identifiers for mapping to your system +const externalOrgId = result.tokenInfo.organizationExternalId; +const externalUserId = result.tokenInfo.userExternalId; +``` + + + + +```python +result = scalekit_client.tokens.validate_token(token=opaque_token) + +# Roles assigned to the user +roles = result[0].token_info.roles + +# External identifiers for mapping to your system +external_org_id = result[0].token_info.organization_external_id +external_user_id = result[0].token_info.user_external_id +``` + + + + +```go +result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) +if err != nil { + log.Printf("Token validation failed: %v", err) + return +} + +// Roles assigned to the user +roles := result.TokenInfo.Roles + +// External identifiers for mapping to your system +externalOrgId := result.TokenInfo.OrganizationExternalId +externalUserId := result.TokenInfo.UserExternalId +``` + + + + +```java +ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + +// Roles assigned to the user +List roles = result.getTokenInfo().getRolesList(); + +// External identifiers for mapping to your system +String externalOrgId = result.getTokenInfo().getOrganizationExternalId(); +String externalUserId = result.getTokenInfo().getUserExternalId(); +``` + + + + + + +### Access custom metadata + +Custom claims attached during token creation are returned in the validation response. Use them for fine-grained authorization decisions without additional database lookups. + + + + +```javascript +const result = await scalekit.token.validateToken(opaqueToken); + +const team = result.tokenInfo.customClaims?.team; +const environment = result.tokenInfo.customClaims?.environment; + +// Use metadata for authorization +if (environment !== 'production') { + return res.status(403).json({ error: 'Production access required' }); +} +``` + + + + +```python +result = scalekit_client.tokens.validate_token(token=opaque_token) + +team = result[0].token_info.custom_claims.get("team") +environment = result[0].token_info.custom_claims.get("environment") + +# Use metadata for authorization +if environment != "production": + return jsonify({"error": "Production access required"}), 403 +``` + + + + +```go +result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) +if err != nil { + log.Printf("Token validation failed: %v", err) + return +} + +team := result.TokenInfo.CustomClaims["team"] +environment := result.TokenInfo.CustomClaims["environment"] + +// Use metadata for authorization +if environment != "production" { + c.JSON(403, gin.H{"error": "Production access required"}) + return +} +``` + + + + +```java +ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + +String team = result.getTokenInfo().getCustomClaimsMap().get("team"); +String environment = result.getTokenInfo().getCustomClaimsMap().get("environment"); + +// Use metadata for authorization +if (!"production".equals(environment)) { + return ResponseEntity.status(403).body(Map.of("error", "Production access required")); +} +``` + + + ## List tokens -Retrieve all active tokens for an organization. Use pagination to handle large result sets, and filter by `userId` to find tokens scoped to a specific user. +Retrieve all active API keys for an organization. Use pagination to handle large result sets, and filter by `userId` to find keys scoped to a specific user. -```javascript title="Express.js" +```javascript // List tokens for an organization const response = await scalekit.token.listTokens(organizationId, { pageSize: 10, @@ -355,7 +541,7 @@ const userTokens = await scalekit.token.listTokens(organizationId, { -```python title="Flask" +```python # List tokens for an organization response = scalekit_client.tokens.list_tokens( organization_id=organization_id, @@ -383,7 +569,7 @@ user_tokens = scalekit_client.tokens.list_tokens( -```go title="Gin" +```go // List tokens for an organization response, err := scalekitClient.Token().ListTokens( ctx, organizationId, scalekit.ListTokensOptions{ @@ -416,7 +602,7 @@ userTokens, err := scalekitClient.Token().ListTokens( -```java title="Spring Boot" +```java // List tokens for an organization ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); @@ -444,52 +630,52 @@ The response includes `totalCount` for the total number of matching tokens and ` ## Invalidate a token -Revoke a token to immediately prevent it from being used. Invalidation takes effect instantly — any subsequent validation request for this token will fail. +Revoke an API key to immediately prevent it from being used. Invalidation takes effect instantly — any subsequent validation request for this key will fail. -This operation is **idempotent**: calling invalidate on an already-revoked token succeeds without error. +This operation is **idempotent**: calling invalidate on an already-revoked key succeeds without error. -```javascript title="Express.js" -// Invalidate by token_id -await scalekit.token.invalidateToken(tokenId); - -// Or invalidate by opaque token string +```javascript +// Invalidate by API key string await scalekit.token.invalidateToken(opaqueToken); + +// Or invalidate by token_id (useful when you store tokenId for lifecycle management) +await scalekit.token.invalidateToken(tokenId); ``` -```python title="Flask" -# Invalidate by token_id -scalekit_client.tokens.invalidate_token(token=token_id) - -# Or invalidate by opaque token string +```python +# Invalidate by API key string scalekit_client.tokens.invalidate_token(token=opaque_token) + +# Or invalidate by token_id (useful when you store token_id for lifecycle management) +scalekit_client.tokens.invalidate_token(token=token_id) ``` -```go title="Gin" -// Invalidate by token_id -err := scalekitClient.Token().InvalidateToken(ctx, tokenId) +```go +// Invalidate by API key string +err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken) -// Or invalidate by opaque token string -err = scalekitClient.Token().InvalidateToken(ctx, opaqueToken) +// Or invalidate by token_id (useful when you store tokenId for lifecycle management) +err = scalekitClient.Token().InvalidateToken(ctx, tokenId) ``` -```java title="Spring Boot" -// Invalidate by token_id -scalekitClient.tokens().invalidate(tokenId); - -// Or invalidate by opaque token string +```java +// Invalidate by API key string scalekitClient.tokens().invalidate(opaqueToken); + +// Or invalidate by token_id (useful when you store tokenId for lifecycle management) +scalekitClient.tokens().invalidate(tokenId); ``` @@ -497,7 +683,7 @@ scalekitClient.tokens().invalidate(opaqueToken); ## Protect your API endpoints -Add token validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned `token_info` for authorization decisions. +Add API key validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned `token_info` for authorization decisions. @@ -662,20 +848,20 @@ public ResponseEntity getResources(HttpServletRequest request) { ## Best practices - - Treat opaque tokens like passwords. Store them in encrypted secrets managers or environment variables. Never log tokens, commit them to version control, or expose them in client-side code. + + We recommend treating API keys like passwords. Store them in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code. - Use the `expiry` parameter for tokens that should automatically become invalid after a set period. This limits the blast radius if a token is compromised. + Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised. Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups. - - To rotate a token: create a new token, update the consuming service to use the new token, verify the new token works, then invalidate the old token. This avoids downtime during rotation. + + To rotate an API key: create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation. ## Next steps -You now have the building blocks to issue, validate, and manage opaque API tokens in your application. For short-lived, scoped authentication between services, see [M2M authentication with client credentials](/authenticate/m2m/api-auth-quickstart/). To control what each token can access, configure [scopes for your M2M clients](/guides/m2m/scopes/). +You now have the building blocks to issue, validate, and manage API keys in your application. For short-lived, scoped authentication between services, see [M2M authentication with client credentials](/authenticate/m2m/api-auth-quickstart/). To control what each key can access, configure [scopes for your M2M clients](/guides/m2m/scopes/). From 704c70653ebf28cb83e69d4f38640dcace054cd5 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Thu, 12 Feb 2026 01:46:13 +0530 Subject: [PATCH 04/18] docs: use specific error types in API keys validate token examples Update all validate token examples to catch SDK-specific exceptions (ScalekitValidateTokenFailureException, ErrTokenValidationFailed, TokenInvalidException) instead of generic Error/Exception, and add bullet-pointed intro section. --- .../docs/authenticate/m2m/api-keys.mdx | 149 +++++++++++------- 1 file changed, 95 insertions(+), 54 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index c7525f1d1..ca9f5faf7 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -18,13 +18,12 @@ head: import { Aside, Steps, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components'; import InstallSDK from '@components/templates/_installsdk.mdx'; -Scalekit API keys let you give your customers long-lived, revocable credentials for programmatic access to your APIs. Each API key is scoped to an organization and optionally to a specific user, so you have fine-grained control over who can access what. Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys are simple bearer strings with no embedded claims — Scalekit handles all validation server-side. +Scalekit API keys are long-lived, revocable bearer credentials for programmatic access to your APIs. Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys have no embedded claims — Scalekit handles all validation server-side. -Scalekit validates every API key in real time, which gives you two security advantages out of the box. Keys can be **validated immediately** after creation — there is no propagation delay. And keys can be **revoked instantly** — once you call invalidate, the very next validation request rejects the key. No waiting for expiry windows or maintaining revocation lists. - -Scalekit API keys also support **user-level scoping**, which enables you to issue personal API keys tied to a specific user within an organization. Your API middleware can extract the `userId` from the validated key to enforce user-specific authorization — enabling personal access tokens, per-user rate limiting, and audit trails tied to individual users. - -We recommend API keys when your customers need persistent credentials for integrations, CI/CD pipelines, service accounts, or any automation that calls your APIs. For short-lived JWTs with scopes and audience restrictions, use [M2M client credentials](/authenticate/m2m/api-auth-quickstart/) instead. +- **Organization and user scoping** — Each key is scoped to an organization and optionally to a specific user, enabling personal access tokens, per-user rate limiting, and audit trails. +- **Real-time validation** — Keys are validated immediately after creation with no propagation delay. +- **Instant revocation** — Once invalidated, the very next validation request rejects the key. No expiry windows or revocation lists. +- **Best for** — Integrations, CI/CD pipelines, service accounts, and any automation that calls your APIs. For short-lived JWTs with scopes, use [M2M client credentials](/authenticate/m2m/api-auth-quickstart/) instead. ```d2 pad=36 shape: sequence_diagram @@ -301,6 +300,8 @@ Scalekit validates the API key server-side and returns its associated context. ```javascript +import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; + try { const result = await scalekit.token.validateToken(opaqueToken); @@ -308,8 +309,10 @@ try { const userId = result.tokenInfo.userId; const claims = result.tokenInfo.customClaims; } catch (error) { - // Token is invalid, expired, or revoked - console.error('Token validation failed:', error.message); + if (error instanceof ScalekitValidateTokenFailureException) { + // Token is invalid, expired, or revoked + console.error('Token validation failed:', error.message); + } } ``` @@ -317,13 +320,15 @@ try { ```python +from scalekit import ScalekitValidateTokenFailureException + try: result = scalekit_client.tokens.validate_token(token=opaque_token) org_id = result[0].token_info.organization_id user_id = result[0].token_info.user_id claims = result[0].token_info.custom_claims -except Exception: +except ScalekitValidateTokenFailureException: # Token is invalid, expired, or revoked print("Token validation failed") ``` @@ -333,7 +338,7 @@ except Exception: ```go result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) -if err != nil { +if errors.Is(err, scalekit.ErrTokenValidationFailed) { // Token is invalid, expired, or revoked log.Printf("Token validation failed: %v", err) return @@ -348,13 +353,15 @@ claims := result.TokenInfo.CustomClaims ```java +import com.scalekit.exceptions.TokenInvalidException; + try { ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); String orgId = result.getTokenInfo().getOrganizationId(); String userId = result.getTokenInfo().getUserId(); Map claims = result.getTokenInfo().getCustomClaimsMap(); -} catch (Exception e) { +} catch (TokenInvalidException e) { // Token is invalid, expired, or revoked System.err.println("Token validation failed: " + e.getMessage()); } @@ -373,28 +380,37 @@ The validated token also includes roles assigned to the user and external identi ```javascript -const result = await scalekit.token.validateToken(opaqueToken); +try { + const result = await scalekit.token.validateToken(opaqueToken); -// Roles assigned to the user -const roles = result.tokenInfo.roles; + // Roles assigned to the user + const roles = result.tokenInfo.roles; -// External identifiers for mapping to your system -const externalOrgId = result.tokenInfo.organizationExternalId; -const externalUserId = result.tokenInfo.userExternalId; + // External identifiers for mapping to your system + const externalOrgId = result.tokenInfo.organizationExternalId; + const externalUserId = result.tokenInfo.userExternalId; +} catch (error) { + if (error instanceof ScalekitValidateTokenFailureException) { + console.error('Token validation failed:', error.message); + } +} ``` ```python -result = scalekit_client.tokens.validate_token(token=opaque_token) +try: + result = scalekit_client.tokens.validate_token(token=opaque_token) -# Roles assigned to the user -roles = result[0].token_info.roles + # Roles assigned to the user + roles = result[0].token_info.roles -# External identifiers for mapping to your system -external_org_id = result[0].token_info.organization_external_id -external_user_id = result[0].token_info.user_external_id + # External identifiers for mapping to your system + external_org_id = result[0].token_info.organization_external_id + external_user_id = result[0].token_info.user_external_id +except ScalekitValidateTokenFailureException: + print("Token validation failed") ``` @@ -402,7 +418,7 @@ external_user_id = result[0].token_info.user_external_id ```go result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) -if err != nil { +if errors.Is(err, scalekit.ErrTokenValidationFailed) { log.Printf("Token validation failed: %v", err) return } @@ -419,14 +435,18 @@ externalUserId := result.TokenInfo.UserExternalId ```java -ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); +try { + ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); -// Roles assigned to the user -List roles = result.getTokenInfo().getRolesList(); + // Roles assigned to the user + List roles = result.getTokenInfo().getRolesList(); -// External identifiers for mapping to your system -String externalOrgId = result.getTokenInfo().getOrganizationExternalId(); -String externalUserId = result.getTokenInfo().getUserExternalId(); + // External identifiers for mapping to your system + String externalOrgId = result.getTokenInfo().getOrganizationExternalId(); + String externalUserId = result.getTokenInfo().getUserExternalId(); +} catch (TokenInvalidException e) { + System.err.println("Token validation failed: " + e.getMessage()); +} ``` @@ -444,14 +464,20 @@ Custom claims attached during token creation are returned in the validation resp ```javascript -const result = await scalekit.token.validateToken(opaqueToken); +try { + const result = await scalekit.token.validateToken(opaqueToken); -const team = result.tokenInfo.customClaims?.team; -const environment = result.tokenInfo.customClaims?.environment; + const team = result.tokenInfo.customClaims?.team; + const environment = result.tokenInfo.customClaims?.environment; -// Use metadata for authorization -if (environment !== 'production') { - return res.status(403).json({ error: 'Production access required' }); + // Use metadata for authorization + if (environment !== 'production') { + return res.status(403).json({ error: 'Production access required' }); + } +} catch (error) { + if (error instanceof ScalekitValidateTokenFailureException) { + console.error('Token validation failed:', error.message); + } } ``` @@ -459,14 +485,17 @@ if (environment !== 'production') { ```python -result = scalekit_client.tokens.validate_token(token=opaque_token) +try: + result = scalekit_client.tokens.validate_token(token=opaque_token) -team = result[0].token_info.custom_claims.get("team") -environment = result[0].token_info.custom_claims.get("environment") + team = result[0].token_info.custom_claims.get("team") + environment = result[0].token_info.custom_claims.get("environment") -# Use metadata for authorization -if environment != "production": - return jsonify({"error": "Production access required"}), 403 + # Use metadata for authorization + if environment != "production": + return jsonify({"error": "Production access required"}), 403 +except ScalekitValidateTokenFailureException: + print("Token validation failed") ``` @@ -474,7 +503,7 @@ if environment != "production": ```go result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) -if err != nil { +if errors.Is(err, scalekit.ErrTokenValidationFailed) { log.Printf("Token validation failed: %v", err) return } @@ -493,14 +522,18 @@ if environment != "production" { ```java -ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); +try { + ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); -String team = result.getTokenInfo().getCustomClaimsMap().get("team"); -String environment = result.getTokenInfo().getCustomClaimsMap().get("environment"); + String team = result.getTokenInfo().getCustomClaimsMap().get("team"); + String environment = result.getTokenInfo().getCustomClaimsMap().get("environment"); -// Use metadata for authorization -if (!"production".equals(environment)) { - return ResponseEntity.status(403).body(Map.of("error", "Production access required")); + // Use metadata for authorization + if (!"production".equals(environment)) { + return ResponseEntity.status(403).body(Map.of("error", "Production access required")); + } +} catch (TokenInvalidException e) { + System.err.println("Token validation failed: " + e.getMessage()); } ``` @@ -689,6 +722,8 @@ Add API key validation as middleware in your API server. Extract the Bearer toke ```javascript title="Express.js" +import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; + async function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; @@ -705,8 +740,11 @@ async function authenticateToken(req, res, next) { req.tokenInfo = result.tokenInfo; next(); } catch (error) { - // Revoked, expired, or malformed tokens are rejected immediately - return res.status(401).json({ error: 'Invalid or expired token' }); + if (error instanceof ScalekitValidateTokenFailureException) { + // Revoked, expired, or malformed tokens are rejected immediately + return res.status(401).json({ error: 'Invalid or expired token' }); + } + throw error; } } @@ -723,6 +761,7 @@ app.get('/api/resources', authenticateToken, (req, res) => { ```python title="Flask" from functools import wraps from flask import request, jsonify, g +from scalekit import ScalekitValidateTokenFailureException def authenticate_token(f): @wraps(f) @@ -739,7 +778,7 @@ def authenticate_token(f): result = scalekit_client.tokens.validate_token(token=token) # Attach token context for downstream handlers g.token_info = result[0].token_info - except Exception: + except ScalekitValidateTokenFailureException: # Revoked, expired, or malformed tokens are rejected immediately return jsonify({"error": "Invalid or expired token"}), 401 @@ -772,7 +811,7 @@ func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc { // Server-side validation — Scalekit checks token status in real time result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token) - if err != nil { + if errors.Is(err, scalekit.ErrTokenValidationFailed) { // Revoked, expired, or malformed tokens are rejected immediately c.JSON(401, gin.H{"error": "Invalid or expired token"}) c.Abort() @@ -797,6 +836,8 @@ r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) ```java title="Spring Boot" +import com.scalekit.exceptions.TokenInvalidException; + @Component public class TokenAuthFilter extends OncePerRequestFilter { private final ScalekitClient scalekitClient; @@ -826,7 +867,7 @@ public class TokenAuthFilter extends OncePerRequestFilter { // Attach token context for downstream handlers request.setAttribute("tokenInfo", result.getTokenInfo()); filterChain.doFilter(request, response); - } catch (Exception e) { + } catch (TokenInvalidException e) { // Revoked, expired, or malformed tokens are rejected immediately response.sendError(401, "Invalid or expired token"); } From 32e3b31df500a637b6df66ac96491ae3fb819d01 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Thu, 12 Feb 2026 02:34:10 +0530 Subject: [PATCH 05/18] docs: adjust API keys tone to match sibling articles and reorder sidebar Rewrite prose sections with a warmer, more conversational tone matching the api-auth-quickstart and scopes articles. Move API keys to last position under "Add auth to your APIs" in the sidebar. --- src/configs/sidebar.config.ts | 2 +- .../docs/authenticate/m2m/api-keys.mdx | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/configs/sidebar.config.ts b/src/configs/sidebar.config.ts index 38cde1228..3d8ece37d 100644 --- a/src/configs/sidebar.config.ts +++ b/src/configs/sidebar.config.ts @@ -67,8 +67,8 @@ export const sidebar = [ items: [ // 'guides/m2m/overview', TODO: It uses M2M context, and older context. Hiding it until we are sure to open it up 'authenticate/m2m/api-auth-quickstart', - 'authenticate/m2m/api-keys', 'guides/m2m/scopes', + 'authenticate/m2m/api-keys', // 'guides/m2m/api-auth-m2m-clients', TODO: Translate this as guides for future ], }, diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index ca9f5faf7..3c305964c 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -18,12 +18,11 @@ head: import { Aside, Steps, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components'; import InstallSDK from '@components/templates/_installsdk.mdx'; -Scalekit API keys are long-lived, revocable bearer credentials for programmatic access to your APIs. Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys have no embedded claims — Scalekit handles all validation server-side. +When your customers need to integrate with your APIs — whether for CI/CD pipelines, partner integrations, or internal tooling — they need a straightforward way to authenticate. Scalekit API keys gives you long-lived, revocable bearer credentials that can be issued to your customers for Organizational or User Level access to your APIs. -- **Organization and user scoping** — Each key is scoped to an organization and optionally to a specific user, enabling personal access tokens, per-user rate limiting, and audit trails. -- **Real-time validation** — Keys are validated immediately after creation with no propagation delay. -- **Instant revocation** — Once invalidated, the very next validation request rejects the key. No expiry windows or revocation lists. -- **Best for** — Integrations, CI/CD pipelines, service accounts, and any automation that calls your APIs. For short-lived JWTs with scopes, use [M2M client credentials](/authenticate/m2m/api-auth-quickstart/) instead. +Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys do not carry embedded claims and cannot be validated on the client side. The API Keys can be validated via APIs with Scalekit and after server side validation, the claims are sent when a valid API Key is presented. Revocation of API keys takes effect immediately with no expiry windows or propagation delays. Each key is scoped to an organization and optionally to a specific user, making it easy to build personal access tokens, per-user rate limiting, and audit trails. + +In this guide, you'll learn how to create, validate, list, and revoke API keys using the Scalekit SDK. ```d2 pad=36 shape: sequence_diagram @@ -113,6 +112,8 @@ ScalekitClient scalekitClient = new ScalekitClient( ## Create a token +To get started, create an API key scoped to an organization. You can optionally scope it to a specific user and attach custom metadata. + ### Organization-scoped API key Create an API key scoped to an organization. This is the most common pattern for service-to-service integrations where the API key represents access on behalf of an entire organization. @@ -289,12 +290,12 @@ The response contains three fields: | Field | Description | |-------|-------------| | `token` | The API key string. **Returned only at creation.** | -| `token_id` | A stable identifier (format: `apit_xxxxx`) for referencing the token in management operations. | +| `token_id` | An identifier (format: `apit_xxxxx`) for referencing the token in management operations. | | `token_info` | Metadata including organization, user, custom claims, and timestamps. | ## Validate a token -Scalekit validates the API key server-side and returns its associated context. +When your API server receives a request with an API key, you'll want to verify it's legitimate before processing the request. Pass the key to Scalekit — it validates the key server-side and returns the associated organization, user, and metadata context. @@ -370,11 +371,11 @@ try { -Validation fails with an error if the API key is invalid, expired, or has been revoked. Use this behavior to reject unauthorized requests in your API middleware. +If the API key is invalid, expired, or has been revoked, validation fails with a specific error that you can catch and handle in your code. This makes it easy to reject unauthorized requests in your API middleware. ### Access roles and organization details -The validated token also includes roles assigned to the user and external identifiers for the organization. Use these to make authorization decisions without additional lookups. +Beyond the basic organization and user information, the validation response also includes any roles assigned to the user and external identifiers you've configured for the organization. These are useful for making authorization decisions without additional database lookups. @@ -458,7 +459,7 @@ try { ### Access custom metadata -Custom claims attached during token creation are returned in the validation response. Use them for fine-grained authorization decisions without additional database lookups. +If you attached custom claims when creating the API key, they come back in every validation response. This is a convenient way to make fine-grained authorization decisions — like restricting access by team or environment — without hitting your database. @@ -542,7 +543,7 @@ try { ## List tokens -Retrieve all active API keys for an organization. Use pagination to handle large result sets, and filter by `userId` to find keys scoped to a specific user. +You can retrieve all active API keys for an organization at any time. The response supports pagination for large result sets, and you can filter by user to find keys scoped to a specific person. @@ -663,9 +664,9 @@ The response includes `totalCount` for the total number of matching tokens and ` ## Invalidate a token -Revoke an API key to immediately prevent it from being used. Invalidation takes effect instantly — any subsequent validation request for this key will fail. +When you need to revoke an API key — for example, when an employee leaves or you suspect credentials have been compromised — you can invalidate it through Scalekit. Revocation takes effect instantly: the very next validation request for that key will fail. -This operation is **idempotent**: calling invalidate on an already-revoked key succeeds without error. +This operation is **idempotent**, so calling invalidate on an already-revoked key succeeds without error. @@ -716,7 +717,7 @@ scalekitClient.tokens().invalidate(tokenId); ## Protect your API endpoints -Add API key validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned `token_info` for authorization decisions. +Now let's put it all together. The most common pattern is to add API key validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned context for authorization decisions. @@ -888,9 +889,11 @@ public ResponseEntity getResources(HttpServletRequest request) { ## Best practices +Here are a few tips to help you get the most out of API keys in production. + - We recommend treating API keys like passwords. Store them in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code. + Recommend your Clients to treat API keys like passwords. They should be stored in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code. Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised. @@ -905,4 +908,4 @@ public ResponseEntity getResources(HttpServletRequest request) { ## Next steps -You now have the building blocks to issue, validate, and manage API keys in your application. For short-lived, scoped authentication between services, see [M2M authentication with client credentials](/authenticate/m2m/api-auth-quickstart/). To control what each key can access, configure [scopes for your M2M clients](/guides/m2m/scopes/). +You now have everything you need to issue, validate, and manage API keys in your application. For short-lived, scoped authentication between services, see [M2M authentication with client credentials](/authenticate/m2m/api-auth-quickstart/). To control what each key can access, configure [scopes for your M2M clients](/guides/m2m/scopes/). From 7403a239eaa8e4911103d88269540cf8e19473ea Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Thu, 12 Feb 2026 02:41:27 +0530 Subject: [PATCH 06/18] docs: add no Co-Authored-By preference to CLAUDE.md --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 288d5afe4..309908c50 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,10 @@ When you need specific guidance, read these files directly: ## Key Reminders +### Git Commits + +- Do NOT include `Co-Authored-By` lines in commit messages + ### SDK Variable Names (CRITICAL) - Node.js: `scalekit` From 98504fd07593901699fddeb8e7c145cfc4b49275 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Thu, 12 Feb 2026 18:12:53 +0530 Subject: [PATCH 07/18] docs: update API authentication quickstart to reflect OAuth 2.0 terminology and enhance clarity - Changed the title from "Add auth to your APIs" to "Add OAuth2.0 to your APIs" for specificity. - Revised introductory content to clarify the role of APIs and the importance of OAuth 2.0 for client credentials authentication. - Improved the description of the API client registration process, emphasizing customer interaction and Scalekit integration. - Added details on API client scopes and their validation, enhancing security context. - Included collapsible sections for sample responses to improve readability and organization. This update aligns the documentation with current standards and enhances user understanding of OAuth 2.0 integration. --- .../authenticate/m2m/api-auth-quickstart.mdx | 336 +++++++++++++++++- .../docs/authenticate/m2m/api-keys.mdx | 4 +- 2 files changed, 327 insertions(+), 13 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx index 1bc90ccb6..37559eb89 100644 --- a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx +++ b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx @@ -1,5 +1,5 @@ --- -title: Add auth to your APIs +title: Add OAuth2.0 to your APIs description: "Secure your APIs in minutes with OAuth 2.0 client credentials, scoped access, and JWT validation using Scalekit" browseCentral: label: "API authentication with M2M OAuth" @@ -19,17 +19,15 @@ head: font-size: var(--sl-text-lg); } sidebar: - label: "Quickstart: API Auth" + label: "Add OAuth 2.0" --- import { LinkButton, Aside, Steps, Tabs, TabItem, Badge } from '@astrojs/starlight/components'; import InstallSDK from '@components/templates/_installsdk.mdx'; -APIs are a fundamental mechanism for systems to communicate and exchange data. By exposing API endpoints, applications enable customers, partners, and external systems to interact seamlessly, creating deeper integrations and increasing the _stickiness_ to your app and it's usecases. +APIs are a way to let your customers, partners, and external systems interact with your application and it's data. It requires authentication to ensure only authorized clients can consume your APIs. Scalekit provides a way to add OAuth2.0 based client credentials authentication to your API endpoints. -Exposing APIs without any authentication is like leaving your door wide open for anyone to walk in. Anyone can access your API endpoints, potentially causing security breaches, data leaks, and unauthorized usage. - -In this guide let's take a look at how your can add OAuth2.0 based client credentials authentication to your APIs to ensure only authorized clients can consume your APIs. +In should here's how it would work: ```d2 shape: sequence_diagram @@ -66,14 +64,17 @@ Your App -> API Client: 7. Returns the protected resource 2. ## Enable API client registration for your customers - To enable secure API access, you must first register an application as an API client. This process generates unique credentials that will authenticate the client when interacting with your API. + Allow your customers to register their applications as API clients. This process generates unique credentials that they can use to authenticate their application when interacting with your API. + + Scalekit will return a client ID and secret that you can show to your customers to integrate their application with your API. + - An Organization ID identifies your customer, and multiple API clients can be registered for the same organization. + - The `POST /organizations/{organization_id}/clients` endpoint creates a new API client for the organization. See [Scalekit API Authentication](/apis/#description/quickstart) to get the `` in case of HTTP requests. - Upon registration, your app obtains a client ID and secret — essentially an application-level authentication mechanism. These credentials should be securely shared with your API client developers for integration (usually via view-once UI). - ```bash title="POST /organizations/{organization_id}/clients" + ```bash title="POST /organizations/{organization_id}/clients" showLineNumbers=false # For authentication details, see: http://docs.scalekit.com/apis#description/authentication curl -L '/api/v1/organizations//clients' \ -H 'Content-Type: application/json' \ @@ -102,7 +103,10 @@ Your App -> API Client: 7. Returns the protected resource }' ``` - ```json title="Sample response" +
+ Sample response + + ```json title="Sample response" showLineNumbers=false { "client": { "client_id": "m2morg_68315758685323697", @@ -142,6 +146,7 @@ Your App -> API Client: 7. Returns the protected resource "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoa..." } ``` +
@@ -178,7 +183,10 @@ Your App -> API Client: 7. Returns the protected resource plain_secret = response.plain_secret ``` - ```json title="Sample response" +
+ Sample response + + ```json title="Sample response" showLineNumbers=false { "client": { "client_id": "m2morg_68315758685323697", @@ -219,6 +227,7 @@ Your App -> API Client: 7. Returns the protected resource } ``` +
@@ -355,4 +364,309 @@ Your App -> API Client: 7. Returns the protected resource Upon successful token verification, your API server gains confidence in the request's legitimacy and can proceed to process the request, leveraging the authorization scopes embedded within the token. +5. ## Verify API client's scopes + + Scopes are embedded in the access token and validated server-side using the Scalekit SDK. This ensures that API clients only access resources they're authorized for, adding an extra layer of security. + + or example, you might create an API client for a customer's deployment service with scopes like `deploy:applications` and `read:deployments`. + + + + + ```bash title="Register an API client with specific scopes" wrap + curl -L 'https:///api/v1/organizations//clients' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{ + "name": "GitHub Actions Deployment Service", + "description": "Service account for GitHub Actions to deploy applications to production", + "scopes": [ + "deploy:applications", + "read:deployments" + ], + "expiry": 3600 + }' + ``` +
+ Sample response + + ```json title="Sample response" showLineNumbers=false + { + "client": { + "client_id": "m2morg_68315758685323697", + "secrets": [ + { + "id": "sks_68315758802764209", + "create_time": "2025-04-16T06:56:05.360Z", + "update_time": "2025-04-16T06:56:05.367190455Z", + "secret_suffix": "UZ0X", + "status": "ACTIVE", + "last_used_time": "2025-04-16T06:56:05.360Z" + } + ], + "name": "GitHub Actions Deployment Service", + "description": "Service account for GitHub Actions to deploy applications to production", + "organization_id": "org_59615193906282635", + "create_time": "2025-04-16T06:56:05.290Z", + "update_time": "2025-04-16T06:56:05.292145150Z", + "scopes": [ + "deploy:applications", + "read:deployments" + ] + }, + "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcfa6reKG1kKNVbqBKW4H5Ctmb5UZ0X" + } + ``` +
+
+ + + ```javascript title="Register an API client with specific scopes" + // Use case: Your customer requests API access for their deployment automation. + // You register an API client app with the appropriate scopes. + import { ScalekitClient } from '@scalekit-sdk/node'; + + // Initialize Scalekit client (see installation guide for setup) + const scalekit = new ScalekitClient( + process.env.SCALEKIT_ENVIRONMENT_URL, + process.env.SCALEKIT_CLIENT_ID, + process.env.SCALEKIT_CLIENT_SECRET + ); + + async function createAPIClient() { + try { + // Define API client details with scopes your customer's app needs + const clientDetails = { + name: 'GitHub Actions Deployment Service', + description: 'Service account for GitHub Actions to deploy applications to production', + scopes: ['deploy:applications', 'read:deployments'], + expiry: 3600, // Token expiry in seconds + }; + + // API call to register the client + const response = await scalekit.m2m.createClient({ + organizationId: process.env.SCALEKIT_ORGANIZATION_ID, + client: clientDetails, + }); + + // Response contains client details and the plain_secret (only returned once) + const clientId = response.client.client_id; + const plainSecret = response.plain_secret; + + // Provide these credentials to your customer securely + console.log('Created API client:', clientId); + } catch (error) { + console.error('Error creating API client:', error); + } + } + + createAPIClient(); + ``` + + + + + ```python title="Register an API client with specific scopes" + # Use case: Your customer requests API access for their deployment automation. + # You register an API client app with the appropriate scopes. + import os + from scalekit import ScalekitClient + + # Initialize Scalekit client (see installation guide for setup) + scalekit_client = ScalekitClient( + env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), + client_id=os.getenv("SCALEKIT_CLIENT_ID"), + client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") + ) + + try: + # Define API client details with scopes your customer's app needs + from scalekit.v1.clients.clients_pb2 import OrganizationClient + + client_details = OrganizationClient( + name="GitHub Actions Deployment Service", + description="Service account for GitHub Actions to deploy applications to production", + scopes=["deploy:applications", "read:deployments"], + expiry=3600 # Token expiry in seconds + ) + + # API call to register the client + response = scalekit_client.m2m_client.create_organization_client( + organization_id=os.getenv("SCALEKIT_ORGANIZATION_ID"), + m2m_client=client_details + ) + + # Response contains client details and the plain_secret (only returned once) + client_id = response.client.client_id + plain_secret = response.plain_secret + + # Provide these credentials to your customer securely + print("Created API client:", client_id) + + except Exception as e: + print("Error creating API client:", e) + ``` + + +
+ + The API returns a JSON object with two key parts: + - `client.client_id` - The client identifier + - `plain_secret` - The client secret (only returned once, never stored by Scalekit) + + Provide both values to your customer securely. Your customer will use these credentials in their application to authenticate with your API. The `plain_secret` is never shown again after creation. + + + +6. ## Validate API client scopes + + When your API server receives a request from an API client app, you must validate the scopes present in the access token provided in the `Authorization` header. The access token is a JSON Web Token (JWT). + + First, let's look at the claims inside a decoded JWT payload. Scalekit encodes the granted scopes in the `scopes` field. + + ```json title="Example decoded access token" showLineNumbers=false {9-12} + { + "client_id": "m2morg_69038819013296423", + "exp": 1745305340, + "iat": 1745218940, + "iss": "", + "jti": "tkn_69041163914445100", + "nbf": 1745218940, + "oid": "org_59615193906282635", + "scopes": [ + "deploy:applications", + "read:deployments" + ], + "sub": "m2morg_69038819013296423" + } + ``` + + + + Your API server should inspect the `scopes` array in the token payload to authorize the requested operation. Here's how you validate the token and check for a specific scope in your API server. + + + + + ```javascript title="Example Express.js middleware for scope validation" collapse={1-27} + // Security: ALWAYS validate the access token on your server before trusting its claims. + // This prevents token forgery and ensures the token has not expired. + import { ScalekitClient } from '@scalekit-sdk/node'; + import jwt from 'jsonwebtoken'; + import jwksClient from 'jwks-rsa'; + + const scalekit = new ScalekitClient( + process.env.SCALEKIT_ENVIRONMENT_URL, + process.env.SCALEKIT_CLIENT_ID, + process.env.SCALEKIT_CLIENT_SECRET + ); + + // Setup JWKS client for token verification + const client = jwksClient({ + jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/.well-known/jwks.json`, + cache: true + }); + + async function getPublicKey(header) { + return new Promise((resolve, reject) => { + client.getSigningKey(header.kid, (err, key) => { + if (err) reject(err); + else resolve(key.getPublicKey()); + }); + }); + } + + async function checkPermissions(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).send('Unauthorized: Missing token'); + } + const token = authHeader.split(' ')[1]; + + try { + // Decode to get the header with kid + const decoded = jwt.decode(token, { complete: true }); + const publicKey = await getPublicKey(decoded.header); + + // Verify the token signature and claims + const verified = jwt.verify(token, publicKey, { + algorithms: ['RS256'], + complete: true + }); + + const decodedToken = verified.payload; + + // Check if the API client app has the required scope + const requiredScope = 'deploy:applications'; + if (decodedToken.scopes && decodedToken.scopes.includes(requiredScope)) { + // API client app has the required scope, proceed with the request + next(); + } else { + // API client app does not have the required scope + res.status(403).send('Forbidden: Insufficient permissions'); + } + } catch (error) { + // Token is invalid or expired + res.status(401).send('Unauthorized: Invalid token'); + } + } + ``` + + + + + ```python title="Example Flask decorator for scope validation" collapse={1-14} + # Security: ALWAYS validate the access token on your server before trusting its claims. + # This prevents token forgery and ensures the token has not expired. + import os + import functools + from scalekit import ScalekitClient + from flask import request, jsonify + + # Initialize Scalekit client + scalekit_client = ScalekitClient( + env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), + client_id=os.getenv("SCALEKIT_CLIENT_ID"), + client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") + ) + + def check_permissions(required_scope): + def decorator(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return jsonify({"error": "Unauthorized: Missing token"}), 401 + + token = auth_header.split(' ')[1] + + try: + # Validate the token using the Scalekit SDK + claims = scalekit_client.validate_access_token_and_get_claims(token=token) + + # Check if the API client app has the required scope + if required_scope in claims.get('scopes', []): + # API client app has the required scope + return f(*args, **kwargs) + else: + # API client app does not have the required scope + return jsonify({"error": "Forbidden: Insufficient permissions"}), 403 + except Exception as e: + # Token is invalid or expired + return jsonify({"error": "Unauthorized: Invalid token"}), 401 + return decorated_function + return decorator + + # Example usage in a Flask route + # @app.route('/deploy', methods=['POST']) + # @check_permissions('deploy:applications') + # def deploy_application(): + # return jsonify({"message": "Deployment successful"}) + ``` + + diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index 3c305964c..345339262 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -2,7 +2,7 @@ title: API keys description: "Issue long-lived, revocable API keys scoped to organizations and users for programmatic access to your APIs" sidebar: - label: "API keys" + label: "Add API Keys" tags: [api-tokens, api-keys, m2m, authentication, bearer-tokens] head: - tag: style @@ -20,7 +20,7 @@ import InstallSDK from '@components/templates/_installsdk.mdx'; When your customers need to integrate with your APIs — whether for CI/CD pipelines, partner integrations, or internal tooling — they need a straightforward way to authenticate. Scalekit API keys gives you long-lived, revocable bearer credentials that can be issued to your customers for Organizational or User Level access to your APIs. -Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys do not carry embedded claims and cannot be validated on the client side. The API Keys can be validated via APIs with Scalekit and after server side validation, the claims are sent when a valid API Key is presented. Revocation of API keys takes effect immediately with no expiry windows or propagation delays. Each key is scoped to an organization and optionally to a specific user, making it easy to build personal access tokens, per-user rate limiting, and audit trails. +Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys do not carry embedded claims and cannot be validated on the client side. The API Keys can be validated via APIs with Scalekit and after server-side validation, the claims are sent when a valid API Key is presented. Revocation of API keys takes effect immediately with no expiry windows or propagation delays. Each key is scoped to an organization and optionally to a specific user, making it easy to build personal access tokens, per-user rate limiting, and audit trails. In this guide, you'll learn how to create, validate, list, and revoke API keys using the Scalekit SDK. From 5c4fc6786278dc0a1995e6ad048e25e0a6bb4dfb Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Thu, 12 Feb 2026 18:15:15 +0530 Subject: [PATCH 08/18] remove scopes as it's merged inot m2m --- src/configs/redirects.config.ts | 1 + src/configs/sidebar.config.ts | 3 +- src/content/docs/guides/m2m/scopes.mdx | 546 ------------------------- 3 files changed, 2 insertions(+), 548 deletions(-) delete mode 100644 src/content/docs/guides/m2m/scopes.mdx diff --git a/src/configs/redirects.config.ts b/src/configs/redirects.config.ts index 83ea77a52..0ef278cad 100644 --- a/src/configs/redirects.config.ts +++ b/src/configs/redirects.config.ts @@ -256,6 +256,7 @@ export const redirects = { '/m2m/api-auth-for-m2m-clients': '/guides/m2m/api-auth-m2m-clients/', '/m2m/external-ids-and-metadata': '/guides/external-ids-and-metadata/', '/m2m/scopes': '/guides/m2m/scopes/', + '/guides/m2m/scopes/': '/authenticate/m2m/api-auth-quickstart/', // ============================================================================= // INTEGRATIONS REDIRECTS diff --git a/src/configs/sidebar.config.ts b/src/configs/sidebar.config.ts index 3d8ece37d..4077f805f 100644 --- a/src/configs/sidebar.config.ts +++ b/src/configs/sidebar.config.ts @@ -66,9 +66,8 @@ export const sidebar = [ label: 'Add auth to your APIs', items: [ // 'guides/m2m/overview', TODO: It uses M2M context, and older context. Hiding it until we are sure to open it up - 'authenticate/m2m/api-auth-quickstart', - 'guides/m2m/scopes', 'authenticate/m2m/api-keys', + 'authenticate/m2m/api-auth-quickstart', // 'guides/m2m/api-auth-m2m-clients', TODO: Translate this as guides for future ], }, diff --git a/src/content/docs/guides/m2m/scopes.mdx b/src/content/docs/guides/m2m/scopes.mdx deleted file mode 100644 index db266ef92..000000000 --- a/src/content/docs/guides/m2m/scopes.mdx +++ /dev/null @@ -1,546 +0,0 @@ ---- -title: Verify API client scopes -description: Define permissions for API client applications using scopes and validate them in your API server -browseCentral: - label: "Define scopes and verify server-side" - filterType: ["tutorial"] - category: ["API authentication"] - icon: "book" -sidebar: - label: Verify API client scopes ---- - -import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; - -Scopes define which permissions an API client application can request when accessing your APIs. When you register an API client for your customers, you assign scopes that determine what actions the client app can perform. Scalekit includes these scopes in the access token when the client app authenticates. - -This guide shows you how to register API clients with specific scopes for your customers and how to validate those scopes in your API server. - - -1. ## Register an API client with scopes - - When you enable API client registration for your customers, you define the scopes that their applications can request. For example, you might create an API client for a customer's deployment service with scopes like `deploy:applications` and `read:deployments`. - - - - - ```javascript title="Register an API client with specific scopes" - // Use case: Your customer requests API access for their deployment automation. - // You register an API client app with the appropriate scopes. - import { ScalekitClient } from '@scalekit-sdk/node'; - - // Initialize Scalekit client (see installation guide for setup) - const scalekit = new ScalekitClient( - process.env.SCALEKIT_ENVIRONMENT_URL, - process.env.SCALEKIT_CLIENT_ID, - process.env.SCALEKIT_CLIENT_SECRET - ); - - async function createAPIClient() { - try { - // Define API client details with scopes your customer's app needs - const clientDetails = { - name: 'GitHub Actions Deployment Service', - description: 'Service account for GitHub Actions to deploy applications to production', - scopes: ['deploy:applications', 'read:deployments'], - expiry: 3600, // Token expiry in seconds - }; - - // API call to register the client - const response = await scalekit.m2m.createClient({ - organizationId: process.env.SCALEKIT_ORGANIZATION_ID, - client: clientDetails, - }); - - // Response contains client details and the plain_secret (only returned once) - const clientId = response.client.client_id; - const plainSecret = response.plain_secret; - - // Provide these credentials to your customer securely - console.log('Created API client:', clientId); - } catch (error) { - console.error('Error creating API client:', error); - } - } - - createAPIClient(); - ``` - - - - - ```python title="Register an API client with specific scopes" - # Use case: Your customer requests API access for their deployment automation. - # You register an API client app with the appropriate scopes. - import os - from scalekit import ScalekitClient - - # Initialize Scalekit client (see installation guide for setup) - scalekit_client = ScalekitClient( - env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), - client_id=os.getenv("SCALEKIT_CLIENT_ID"), - client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") - ) - - try: - # Define API client details with scopes your customer's app needs - from scalekit.v1.clients.clients_pb2 import OrganizationClient - - client_details = OrganizationClient( - name="GitHub Actions Deployment Service", - description="Service account for GitHub Actions to deploy applications to production", - scopes=["deploy:applications", "read:deployments"], - expiry=3600 # Token expiry in seconds - ) - - # API call to register the client - response = scalekit_client.m2m_client.create_organization_client( - organization_id=os.getenv("SCALEKIT_ORGANIZATION_ID"), - m2m_client=client_details - ) - - # Response contains client details and the plain_secret (only returned once) - client_id = response.client.client_id - plain_secret = response.plain_secret - - # Provide these credentials to your customer securely - print("Created API client:", client_id) - - except Exception as e: - print("Error creating API client:", e) - ``` - - - - - ```go title="Register an API client with specific scopes" - // Use case: Your customer requests API access for their deployment automation. - // You register an API client app with the appropriate scopes. - package main - - import ( - "context" - "fmt" - "os" - "time" - - "github.com/scalekit-inc/scalekit-sdk-go" - "github.com/scalekit-inc/scalekit-sdk-go/pkg/grpc/organizations" - ) - - func main() { - // Initialize Scalekit client (see installation guide for setup) - scalekitClient := scalekit.NewScalekitClient( - os.Getenv("SCALEKIT_ENVIRONMENT_URL"), - os.Getenv("SCALEKIT_CLIENT_ID"), - os.Getenv("SCALEKIT_CLIENT_SECRET"), - ) - - ctx := context.Background() - - // Define API client details with scopes your customer's app needs - clientDetails := &organizations.OrganizationClient{ - Name: "GitHub Actions Deployment Service", - Description: "Service account for GitHub Actions to deploy applications to production", - Scopes: []string{"deploy:applications", "read:deployments"}, - Expiry: int64(time.Hour.Seconds()), // Token expiry in seconds - } - - // API call to register the client - response, err := scalekitClient.Organization.CreateOrganizationClient( - ctx, - os.Getenv("SCALEKIT_ORGANIZATION_ID"), - clientDetails, - ) - if err != nil { - fmt.Println("Error creating API client:", err) - return - } - - // Response contains client details and the plain_secret (only returned once) - clientID := response.Client.ClientId - plainSecret := response.PlainSecret - - // Provide these credentials to your customer securely - fmt.Println("Created API client:", clientID) - } - ``` - - - - - ```java title="Register an API client with specific scopes" - // Use case: Your customer requests API access for their deployment automation. - // You register an API client app with the appropriate scopes. - import com.scalekit.ScalekitClient; - import com.scalekit.grpc.scalekit.v1.organizations.OrganizationClient; - import com.scalekit.grpc.scalekit.v1.organizations.CreateOrganizationClientResponse; - import java.util.Arrays; - - public class CreateAPIClient { - public static void main(String[] args) { - // Initialize Scalekit client (see installation guide for setup) - ScalekitClient scalekitClient = new ScalekitClient( - System.getenv("SCALEKIT_ENVIRONMENT_URL"), - System.getenv("SCALEKIT_CLIENT_ID"), - System.getenv("SCALEKIT_CLIENT_SECRET") - ); - - try { - // Define API client details with scopes your customer's app needs - OrganizationClient clientDetails = OrganizationClient.newBuilder() - .setName("GitHub Actions Deployment Service") - .setDescription("Service account for GitHub Actions to deploy applications to production") - .addAllScopes(Arrays.asList("deploy:applications", "read:deployments")) - .setExpiry(3600L) // Token expiry in seconds - .build(); - - // API call to register the client - CreateOrganizationClientResponse response = scalekitClient - .organizations() - .createOrganizationClient( - System.getenv("SCALEKIT_ORGANIZATION_ID"), - clientDetails - ); - - // Response contains client details and the plain_secret (only returned once) - String clientId = response.getClient().getClientId(); - String plainSecret = response.getPlainSecret(); - - // Provide these credentials to your customer securely - System.out.println("Created API client: " + clientId); - - } catch (Exception e) { - System.err.println("Error creating API client: " + e.getMessage()); - } - } - } - ``` - - - - - ```bash title="Register an API client with specific scopes" wrap - curl -L 'https:///api/v1/organizations//clients' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer ' \ - -d '{ - "name": "GitHub Actions Deployment Service", - "description": "Service account for GitHub Actions to deploy applications to production", - "scopes": [ - "deploy:applications", - "read:deployments" - ], - "expiry": 3600 - }' - ``` - - ```json title="Sample response" - { - "client": { - "client_id": "m2morg_68315758685323697", - "secrets": [ - { - "id": "sks_68315758802764209", - "create_time": "2025-04-16T06:56:05.360Z", - "update_time": "2025-04-16T06:56:05.367190455Z", - "secret_suffix": "UZ0X", - "status": "ACTIVE", - "last_used_time": "2025-04-16T06:56:05.360Z" - } - ], - "name": "GitHub Actions Deployment Service", - "description": "Service account for GitHub Actions to deploy applications to production", - "organization_id": "org_59615193906282635", - "create_time": "2025-04-16T06:56:05.290Z", - "update_time": "2025-04-16T06:56:05.292145150Z", - "scopes": [ - "deploy:applications", - "read:deployments" - ] - }, - "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcfa6reKG1kKNVbqBKW4H5Ctmb5UZ0X" - } - ``` - - - - - - - - -2. ## Validate API client scopes - - When your API server receives a request from an API client app, you must validate the scopes present in the access token provided in the `Authorization` header. The access token is a JSON Web Token (JWT). - - First, let's look at the claims inside a decoded JWT payload. Scalekit encodes the granted scopes in the `scopes` field. - - ```json title="Example decoded access token" showLineNumbers=false - { - "client_id": "m2morg_69038819013296423", - "exp": 1745305340, - "iat": 1745218940, - "iss": "", - "jti": "tkn_69041163914445100", - "nbf": 1745218940, - "oid": "org_59615193906282635", - "scopes": [ - "deploy:applications", - "read:deployments" - ], - "sub": "m2morg_69038819013296423" - } - ``` - - - - Your API server should inspect the `scopes` array in the token payload to authorize the requested operation. Here's how you validate the token and check for a specific scope in your API server. - - - - - ```javascript title="Example Express.js middleware for scope validation" - // Security: ALWAYS validate the access token on your server before trusting its claims. - // This prevents token forgery and ensures the token has not expired. - import { ScalekitClient } from '@scalekit-sdk/node'; - import jwt from 'jsonwebtoken'; - import jwksClient from 'jwks-rsa'; - - const scalekit = new ScalekitClient( - process.env.SCALEKIT_ENVIRONMENT_URL, - process.env.SCALEKIT_CLIENT_ID, - process.env.SCALEKIT_CLIENT_SECRET - ); - - // Setup JWKS client for token verification - const client = jwksClient({ - jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/.well-known/jwks.json`, - cache: true - }); - - async function getPublicKey(header) { - return new Promise((resolve, reject) => { - client.getSigningKey(header.kid, (err, key) => { - if (err) reject(err); - else resolve(key.getPublicKey()); - }); - }); - } - - async function checkPermissions(req, res, next) { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).send('Unauthorized: Missing token'); - } - const token = authHeader.split(' ')[1]; - - try { - // Decode to get the header with kid - const decoded = jwt.decode(token, { complete: true }); - const publicKey = await getPublicKey(decoded.header); - - // Verify the token signature and claims - const verified = jwt.verify(token, publicKey, { - algorithms: ['RS256'], - complete: true - }); - - const decodedToken = verified.payload; - - // Check if the API client app has the required scope - const requiredScope = 'deploy:applications'; - if (decodedToken.scopes && decodedToken.scopes.includes(requiredScope)) { - // API client app has the required scope, proceed with the request - next(); - } else { - // API client app does not have the required scope - res.status(403).send('Forbidden: Insufficient permissions'); - } - } catch (error) { - // Token is invalid or expired - res.status(401).send('Unauthorized: Invalid token'); - } - } - ``` - - - - - ```python title="Example Flask decorator for scope validation" - # Security: ALWAYS validate the access token on your server before trusting its claims. - # This prevents token forgery and ensures the token has not expired. - import os - import functools - from scalekit import ScalekitClient - from flask import request, jsonify - - # Initialize Scalekit client - scalekit_client = ScalekitClient( - env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), - client_id=os.getenv("SCALEKIT_CLIENT_ID"), - client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") - ) - - def check_permissions(required_scope): - def decorator(f): - @functools.wraps(f) - def decorated_function(*args, **kwargs): - auth_header = request.headers.get('Authorization') - if not auth_header or not auth_header.startswith('Bearer '): - return jsonify({"error": "Unauthorized: Missing token"}), 401 - - token = auth_header.split(' ')[1] - - try: - # Validate the token using the Scalekit SDK - claims = scalekit_client.validate_access_token_and_get_claims(token=token) - - # Check if the API client app has the required scope - if required_scope in claims.get('scopes', []): - # API client app has the required scope - return f(*args, **kwargs) - else: - # API client app does not have the required scope - return jsonify({"error": "Forbidden: Insufficient permissions"}), 403 - except Exception as e: - # Token is invalid or expired - return jsonify({"error": "Unauthorized: Invalid token"}), 401 - return decorated_function - return decorator - - # Example usage in a Flask route - # @app.route('/deploy', methods=['POST']) - # @check_permissions('deploy:applications') - # def deploy_application(): - # return jsonify({"message": "Deployment successful"}) - ``` - - - - - ```go title="Example Go middleware for scope validation" - // Security: ALWAYS validate the access token on your server before trusting its claims. - // This prevents token forgery and ensures the token has not expired. - package main - - import ( - "context" - "net/http" - "os" - "strings" - - "github.com/scalekit-inc/scalekit-sdk-go" - ) - - func CheckPermissions(next http.Handler, requiredScope string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { - http.Error(w, "Unauthorized: Missing token", http.StatusUnauthorized) - return - } - token := strings.TrimPrefix(authHeader, "Bearer ") - - // Initialize Scalekit client (should be a singleton in a real app) - scalekitClient := scalekit.NewScalekitClient( - os.Getenv("SCALEKIT_ENVIRONMENT_URL"), - os.Getenv("SCALEKIT_CLIENT_ID"), - os.Getenv("SCALEKIT_CLIENT_SECRET"), - ) - - // Validate the token using the Scalekit SDK - decodedToken, err := scalekitClient.Token.VerifyToken(context.Background(), token, nil) - if err != nil { - http.Error(w, "Unauthorized: Invalid token", http.StatusUnauthorized) - return - } - - // Check if the API client app has the required scope - hasScope := false - for _, scope := range decodedToken.Claims.Scopes { - if scope == requiredScope { - hasScope = true - break - } - } - - if hasScope { - // API client app has the required scope - next.ServeHTTP(w, r) - } else { - // API client app does not have the required scope - http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden) - } - }) - } - ``` - - - - - ```java title="Example Spring Boot interceptor for scope validation" - // Security: ALWAYS validate the access token on your server before trusting its claims. - // This prevents token forgery and ensures the token has not expired. - import com.scalekit.ScalekitClient; - import com.scalekit.internal.http.TokenClaims; - import org.springframework.stereotype.Component; - import org.springframework.web.servlet.HandlerInterceptor; - import javax.servlet.http.HttpServletRequest; - import javax.servlet.http.HttpServletResponse; - - @Component - public class PermissionInterceptor implements HandlerInterceptor { - - private final ScalekitClient scalekitClient; - - public PermissionInterceptor(ScalekitClient scalekitClient) { - this.scalekitClient = scalekitClient; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String authHeader = request.getHeader("Authorization"); - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Missing token"); - return false; - } - String token = authHeader.substring(7); - - try { - // Validate the token using the Scalekit SDK - TokenClaims claims = scalekitClient.token().verifyToken(token, null); - - // Check if the API client app has the required scope - String requiredScope = "deploy:applications"; - if (claims.getScopes() != null && claims.getScopes().contains(requiredScope)) { - // API client app has the required scope - return true; - } else { - // API client app does not have the required scope - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden: Insufficient permissions"); - return false; - } - } catch (Exception e) { - // Token is invalid or expired - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Invalid token"); - return false; - } - } - } - ``` - - - - - From 4d8e7e7198703c48709bfc7f212cbb2513edcff5 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Thu, 12 Feb 2026 18:16:41 +0530 Subject: [PATCH 09/18] fix the next steps --- src/content/docs/authenticate/m2m/api-keys.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index 345339262..1cb675513 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -906,6 +906,4 @@ Here are a few tips to help you get the most out of API keys in production.
-## Next steps - You now have everything you need to issue, validate, and manage API keys in your application. For short-lived, scoped authentication between services, see [M2M authentication with client credentials](/authenticate/m2m/api-auth-quickstart/). To control what each key can access, configure [scopes for your M2M clients](/guides/m2m/scopes/). From c9e22a541bc766bb46095dc68fb45bb37de168d1 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Thu, 12 Feb 2026 19:14:15 +0530 Subject: [PATCH 10/18] switch: api key and oauth2 --- src/configs/sidebar.config.ts | 2 +- .../authenticate/m2m/api-auth-quickstart.mdx | 4 +-- .../docs/authenticate/m2m/api-keys.mdx | 28 ++++++------------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/configs/sidebar.config.ts b/src/configs/sidebar.config.ts index 4077f805f..cfdc9fae3 100644 --- a/src/configs/sidebar.config.ts +++ b/src/configs/sidebar.config.ts @@ -66,8 +66,8 @@ export const sidebar = [ label: 'Add auth to your APIs', items: [ // 'guides/m2m/overview', TODO: It uses M2M context, and older context. Hiding it until we are sure to open it up - 'authenticate/m2m/api-keys', 'authenticate/m2m/api-auth-quickstart', + 'authenticate/m2m/api-keys', // 'guides/m2m/api-auth-m2m-clients', TODO: Translate this as guides for future ], }, diff --git a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx index 37559eb89..fb4d56aad 100644 --- a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx +++ b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx @@ -364,7 +364,7 @@ Your App -> API Client: 7. Returns the protected resource Upon successful token verification, your API server gains confidence in the request's legitimacy and can proceed to process the request, leveraging the authorization scopes embedded within the token. -5. ## Verify API client's scopes +5. ## Register API client's scopes Scopes are embedded in the access token and validated server-side using the Scalekit SDK. This ensures that API clients only access resources they're authorized for, adding an extra layer of security. @@ -520,7 +520,7 @@ Your App -> API Client: 7. Returns the protected resource You can also include `custom_claims` (key-value metadata) and `audience` (target API endpoints) when registering API clients. See the [quickstart guide](/authenticate/m2m/api-auth-quickstart) for examples. -6. ## Validate API client scopes +6. ## Verify API client's scopes When your API server receives a request from an API client app, you must validate the scopes present in the access token provided in the `Authorization` header. The access token is a JSON Web Token (JWT). diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index 1cb675513..b42faeeec 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -887,23 +887,11 @@ public ResponseEntity getResources(HttpServletRequest request) {
-## Best practices - -Here are a few tips to help you get the most out of API keys in production. - - - - Recommend your Clients to treat API keys like passwords. They should be stored in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code. - - - Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised. - - - Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups. - - - To rotate an API key: create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation. - - - -You now have everything you need to issue, validate, and manage API keys in your application. For short-lived, scoped authentication between services, see [M2M authentication with client credentials](/authenticate/m2m/api-auth-quickstart/). To control what each key can access, configure [scopes for your M2M clients](/guides/m2m/scopes/). +Here are a few tips to help you get the most out of API keys in production: + +- **Store API keys securely**: Treat API keys like passwords. Store them in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code. +- **Set expiry for time-limited access**: Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised. +- **Use custom claims for context**: Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups. +- **Rotate keys safely**: To rotate an API key, create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation. + +You now have everything you need to issue, validate, and manage API keys in your application. \ No newline at end of file From ea6e0d823e7ae600c8b83b08458ba0be3465c204 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Thu, 12 Feb 2026 19:21:05 +0530 Subject: [PATCH 11/18] remove the next link --- src/content/docs/guides/m2m/api-auth-m2m-clients.mdx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx b/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx index a9320f4cd..4b67e0b0e 100644 --- a/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx +++ b/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx @@ -8,12 +8,6 @@ browseCentral: icon: "book" sidebar: label: Authenticate customer apps -prev: - link: '/guides/m2m/scopes/' - label: 'Define & validate scopes' -next: - link: '/authenticate/mcp/intro-to-mcp-auth/' - label: 'Understanding MCP authentication' seeAlso: label: "Code samples" expanded: true From 7808ce75c0490a68f0e0ed9a885e10959ce1ac73 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Mon, 23 Feb 2026 08:44:49 +0530 Subject: [PATCH 12/18] fix: add optional chaining for tokenInfo in Node.js examples, use getters for Go optional proto fields, add missing Token import in Java middleware --- .../docs/authenticate/m2m/api-keys.mdx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index b42faeeec..367836ea2 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -306,9 +306,9 @@ import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; try { const result = await scalekit.token.validateToken(opaqueToken); - const orgId = result.tokenInfo.organizationId; - const userId = result.tokenInfo.userId; - const claims = result.tokenInfo.customClaims; + const orgId = result.tokenInfo?.organizationId; + const userId = result.tokenInfo?.userId; + const claims = result.tokenInfo?.customClaims; } catch (error) { if (error instanceof ScalekitValidateTokenFailureException) { // Token is invalid, expired, or revoked @@ -346,7 +346,7 @@ if errors.Is(err, scalekit.ErrTokenValidationFailed) { } orgId := result.TokenInfo.OrganizationId -userId := result.TokenInfo.UserId +userId := result.TokenInfo.GetUserId() // *string — nil for org-scoped tokens claims := result.TokenInfo.CustomClaims ``` @@ -385,11 +385,11 @@ try { const result = await scalekit.token.validateToken(opaqueToken); // Roles assigned to the user - const roles = result.tokenInfo.roles; + const roles = result.tokenInfo?.roles; // External identifiers for mapping to your system - const externalOrgId = result.tokenInfo.organizationExternalId; - const externalUserId = result.tokenInfo.userExternalId; + const externalOrgId = result.tokenInfo?.organizationExternalId; + const externalUserId = result.tokenInfo?.userExternalId; } catch (error) { if (error instanceof ScalekitValidateTokenFailureException) { console.error('Token validation failed:', error.message); @@ -429,7 +429,7 @@ roles := result.TokenInfo.Roles // External identifiers for mapping to your system externalOrgId := result.TokenInfo.OrganizationExternalId -externalUserId := result.TokenInfo.UserExternalId +externalUserId := result.TokenInfo.GetUserExternalId() // *string — nil if no external ID ```
@@ -468,8 +468,8 @@ If you attached custom claims when creating the API key, they come back in every try { const result = await scalekit.token.validateToken(opaqueToken); - const team = result.tokenInfo.customClaims?.team; - const environment = result.tokenInfo.customClaims?.environment; + const team = result.tokenInfo?.customClaims?.team; + const environment = result.tokenInfo?.customClaims?.environment; // Use metadata for authorization if (environment !== 'production') { @@ -838,6 +838,7 @@ r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) ```java title="Spring Boot" import com.scalekit.exceptions.TokenInvalidException; +import com.scalekit.grpc.scalekit.v1.tokens.Token; @Component public class TokenAuthFilter extends OncePerRequestFilter { From 3e056ac4f5e6ba37f098329f033824ffb7e83027 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Mon, 23 Feb 2026 08:56:04 +0530 Subject: [PATCH 13/18] fix: add missing Java imports, fix Go middleware nil-dereference, add error handling to list/invalidate examples across all languages --- .../docs/authenticate/m2m/api-keys.mdx | 164 ++++++++++++------ 1 file changed, 112 insertions(+), 52 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index 367836ea2..9952c8508 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -177,6 +177,8 @@ tokenId := response.TokenId ```java +import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse; + try { CreateTokenResponse response = scalekitClient.tokens().create(organizationId); @@ -265,6 +267,9 @@ tokenId := userToken.TokenId ```java +import java.util.Map; +import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse; + try { Map customClaims = Map.of( "team", "engineering", @@ -354,7 +359,9 @@ claims := result.TokenInfo.CustomClaims ```java +import java.util.Map; import com.scalekit.exceptions.TokenInvalidException; +import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; try { ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); @@ -436,6 +443,10 @@ externalUserId := result.TokenInfo.GetUserExternalId() // *string — nil if no ```java +import java.util.List; +import com.scalekit.exceptions.TokenInvalidException; +import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; + try { ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); @@ -523,6 +534,10 @@ if environment != "production" { ```java +import java.util.Map; +import com.scalekit.exceptions.TokenInvalidException; +import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; + try { ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); @@ -549,55 +564,62 @@ You can retrieve all active API keys for an organization at any time. The respon ```javascript -// List tokens for an organization -const response = await scalekit.token.listTokens(organizationId, { - pageSize: 10, -}); +try { + // List tokens for an organization + const response = await scalekit.token.listTokens(organizationId, { + pageSize: 10, + }); -for (const token of response.tokens) { - console.log(token.tokenId, token.description); -} + for (const token of response.tokens) { + console.log(token.tokenId, token.description); + } -// Paginate through results -if (response.nextPageToken) { - const nextPage = await scalekit.token.listTokens(organizationId, { - pageSize: 10, - pageToken: response.nextPageToken, + // Paginate through results + if (response.nextPageToken) { + const nextPage = await scalekit.token.listTokens(organizationId, { + pageSize: 10, + pageToken: response.nextPageToken, + }); + } + + // Filter tokens by user + const userTokens = await scalekit.token.listTokens(organizationId, { + userId: 'usr_12345', }); +} catch (error) { + console.error('Failed to list tokens:', error.message); } - -// Filter tokens by user -const userTokens = await scalekit.token.listTokens(organizationId, { - userId: 'usr_12345', -}); ``` ```python -# List tokens for an organization -response = scalekit_client.tokens.list_tokens( - organization_id=organization_id, - page_size=10, -) - -for token in response[0].tokens: - print(token.token_id, token.description) - -# Paginate through results -if response[0].next_page_token: - next_page = scalekit_client.tokens.list_tokens( +try: + # List tokens for an organization + response = scalekit_client.tokens.list_tokens( organization_id=organization_id, page_size=10, - page_token=response[0].next_page_token, ) -# Filter tokens by user -user_tokens = scalekit_client.tokens.list_tokens( - organization_id=organization_id, - user_id="usr_12345", -) + for token in response[0].tokens: + print(token.token_id, token.description) + + # Paginate through results + if response[0].next_page_token: + next_page = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + page_size=10, + page_token=response[0].next_page_token, + ) + + # Filter tokens by user + user_tokens = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + user_id="usr_12345", + ) +except Exception as e: + print(f"Failed to list tokens: {e}") ``` @@ -610,6 +632,10 @@ response, err := scalekitClient.Token().ListTokens( PageSize: 10, }, ) +if err != nil { + log.Printf("Failed to list tokens: %v", err) + return +} for _, token := range response.Tokens { fmt.Println(token.TokenId, token.GetDescription()) @@ -623,6 +649,11 @@ if response.NextPageToken != "" { PageToken: response.NextPageToken, }, ) + if err != nil { + log.Printf("Failed to fetch next page: %v", err) + return + } + _ = nextPage // process nextPage.Tokens } // Filter tokens by user @@ -631,12 +662,20 @@ userTokens, err := scalekitClient.Token().ListTokens( UserId: "usr_12345", }, ) +if err != nil { + log.Printf("Failed to list user tokens: %v", err) + return +} +_ = userTokens // process userTokens.Tokens ``` ```java +import com.scalekit.grpc.scalekit.v1.tokens.ListTokensResponse; +import com.scalekit.grpc.scalekit.v1.tokens.Token; + // List tokens for an organization ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); @@ -672,22 +711,29 @@ This operation is **idempotent**, so calling invalidate on an already-revoked ke ```javascript -// Invalidate by API key string -await scalekit.token.invalidateToken(opaqueToken); +try { + // Invalidate by API key string + await scalekit.token.invalidateToken(opaqueToken); -// Or invalidate by token_id (useful when you store tokenId for lifecycle management) -await scalekit.token.invalidateToken(tokenId); + // Or invalidate by token_id (useful when you store tokenId for lifecycle management) + await scalekit.token.invalidateToken(tokenId); +} catch (error) { + console.error('Failed to invalidate token:', error.message); +} ``` ```python -# Invalidate by API key string -scalekit_client.tokens.invalidate_token(token=opaque_token) +try: + # Invalidate by API key string + scalekit_client.tokens.invalidate_token(token=opaque_token) -# Or invalidate by token_id (useful when you store token_id for lifecycle management) -scalekit_client.tokens.invalidate_token(token=token_id) + # Or invalidate by token_id (useful when you store token_id for lifecycle management) + scalekit_client.tokens.invalidate_token(token=token_id) +except Exception as e: + print(f"Failed to invalidate token: {e}") ``` @@ -695,21 +741,29 @@ scalekit_client.tokens.invalidate_token(token=token_id) ```go // Invalidate by API key string -err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken) +if err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken); err != nil { + log.Printf("Failed to invalidate token: %v", err) +} // Or invalidate by token_id (useful when you store tokenId for lifecycle management) -err = scalekitClient.Token().InvalidateToken(ctx, tokenId) +if err := scalekitClient.Token().InvalidateToken(ctx, tokenId); err != nil { + log.Printf("Failed to invalidate token: %v", err) +} ``` ```java -// Invalidate by API key string -scalekitClient.tokens().invalidate(opaqueToken); +try { + // Invalidate by API key string + scalekitClient.tokens().invalidate(opaqueToken); -// Or invalidate by token_id (useful when you store tokenId for lifecycle management) -scalekitClient.tokens().invalidate(tokenId); + // Or invalidate by token_id (useful when you store tokenId for lifecycle management) + scalekitClient.tokens().invalidate(tokenId); +} catch (Exception e) { + System.err.println("Failed to invalidate token: " + e.getMessage()); +} ``` @@ -812,9 +866,14 @@ func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc { // Server-side validation — Scalekit checks token status in real time result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token) - if errors.Is(err, scalekit.ErrTokenValidationFailed) { - // Revoked, expired, or malformed tokens are rejected immediately - c.JSON(401, gin.H{"error": "Invalid or expired token"}) + if err != nil { + if errors.Is(err, scalekit.ErrTokenValidationFailed) { + // Revoked, expired, or malformed tokens are rejected immediately + c.JSON(401, gin.H{"error": "Invalid or expired token"}) + } else { + // Surface transport or unexpected errors as 500 + c.JSON(500, gin.H{"error": "Internal server error"}) + } c.Abort() return } @@ -839,6 +898,7 @@ r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) ```java title="Spring Boot" import com.scalekit.exceptions.TokenInvalidException; import com.scalekit.grpc.scalekit.v1.tokens.Token; +import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; @Component public class TokenAuthFilter extends OncePerRequestFilter { From e2fb2721cfed1307244e4f69b61d12b707692cd0 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Mon, 23 Feb 2026 16:15:24 +0530 Subject: [PATCH 14/18] fix: remove [0] tuple indexing from Python examples Python SDK's token.py dropped .with_call so methods now return plain proto response objects instead of (response, call) tuples. Update all Python code examples in the API keys guide to remove [0] indexing. --- .../docs/authenticate/m2m/api-keys.mdx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index 9952c8508..de461768e 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -146,9 +146,8 @@ try: description="CI/CD pipeline token", ) - # SDK returns (response, metadata) tuple — access response at index 0 - opaque_token = response[0].token # store this securely - token_id = response[0].token_id # format: apit_xxxxx + opaque_token = response.token # store this securely + token_id = response.token_id # format: apit_xxxxx except Exception as e: print(f"Failed to create token: {e}") ``` @@ -234,8 +233,8 @@ try: description="Deployment service token", ) - opaque_token = user_token[0].token - token_id = user_token[0].token_id + opaque_token = user_token.token + token_id = user_token.token_id except Exception as e: print(f"Failed to create token: {e}") ``` @@ -331,9 +330,9 @@ from scalekit import ScalekitValidateTokenFailureException try: result = scalekit_client.tokens.validate_token(token=opaque_token) - org_id = result[0].token_info.organization_id - user_id = result[0].token_info.user_id - claims = result[0].token_info.custom_claims + org_id = result.token_info.organization_id + user_id = result.token_info.user_id + claims = result.token_info.custom_claims except ScalekitValidateTokenFailureException: # Token is invalid, expired, or revoked print("Token validation failed") @@ -412,11 +411,11 @@ try: result = scalekit_client.tokens.validate_token(token=opaque_token) # Roles assigned to the user - roles = result[0].token_info.roles + roles = result.token_info.roles # External identifiers for mapping to your system - external_org_id = result[0].token_info.organization_external_id - external_user_id = result[0].token_info.user_external_id + external_org_id = result.token_info.organization_external_id + external_user_id = result.token_info.user_external_id except ScalekitValidateTokenFailureException: print("Token validation failed") ``` @@ -500,8 +499,8 @@ try { try: result = scalekit_client.tokens.validate_token(token=opaque_token) - team = result[0].token_info.custom_claims.get("team") - environment = result[0].token_info.custom_claims.get("environment") + team = result.token_info.custom_claims.get("team") + environment = result.token_info.custom_claims.get("environment") # Use metadata for authorization if environment != "production": @@ -602,15 +601,15 @@ try: page_size=10, ) - for token in response[0].tokens: + for token in response.tokens: print(token.token_id, token.description) # Paginate through results - if response[0].next_page_token: + if response.next_page_token: next_page = scalekit_client.tokens.list_tokens( organization_id=organization_id, page_size=10, - page_token=response[0].next_page_token, + page_token=response.next_page_token, ) # Filter tokens by user @@ -832,7 +831,7 @@ def authenticate_token(f): # Server-side validation — Scalekit checks token status in real time result = scalekit_client.tokens.validate_token(token=token) # Attach token context for downstream handlers - g.token_info = result[0].token_info + g.token_info = result.token_info except ScalekitValidateTokenFailureException: # Revoked, expired, or malformed tokens are rejected immediately return jsonify({"error": "Invalid or expired token"}), 401 From f48e16f7d9af75b1159198ea834c8daa1197c884 Mon Sep 17 00:00:00 2001 From: Hrishikesh Premkumar Date: Wed, 25 Feb 2026 16:49:11 +0530 Subject: [PATCH 15/18] fix: address CodeRabbit review comments on API keys docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - redirects: collapse two-hop /m2m/scopes redirect to point directly to api-auth-quickstart - api-auth-quickstart: replace hardcoded docs.scalekit.com URL with root-relative path - api-auth-quickstart: update SDK support note to list all four SDKs (was Python-only) - api-auth-quickstart: change BrowseCentral label to action-oriented 'Secure APIs with OAuth' - api-auth-quickstart: fix grammar (it's→its, OAuth2.0→OAuth 2.0-based, malformed sentence, or example→For example) - api-keys: fix intro grammar (gives→give, User Level→user-level, simplify second sentence) - api-keys: wrap Java list-tokens example in try-catch to match error-handling pattern - api-keys: add 'What's next' CardGrid with links to related pages --- src/configs/redirects.config.ts | 2 +- .../authenticate/m2m/api-auth-quickstart.mdx | 12 ++-- .../docs/authenticate/m2m/api-keys.mdx | 55 +++++++++++++------ 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/configs/redirects.config.ts b/src/configs/redirects.config.ts index 6714baab6..3826bcffb 100644 --- a/src/configs/redirects.config.ts +++ b/src/configs/redirects.config.ts @@ -273,7 +273,7 @@ export const redirects = { '/m2m/authenticate-scalekit-api': '/guides/authenticate-scalekit-api/', '/m2m/api-auth-for-m2m-clients': '/guides/m2m/api-auth-m2m-clients/', '/m2m/external-ids-and-metadata': '/guides/external-ids-and-metadata/', - '/m2m/scopes': '/guides/m2m/scopes/', + '/m2m/scopes': '/authenticate/m2m/api-auth-quickstart/', '/guides/m2m/scopes/': '/authenticate/m2m/api-auth-quickstart/', '/reference/api/m2m-clients': '/guides/m2m/api-auth-m2m-clients/', diff --git a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx index fb4d56aad..12a6a547c 100644 --- a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx +++ b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx @@ -2,7 +2,7 @@ title: Add OAuth2.0 to your APIs description: "Secure your APIs in minutes with OAuth 2.0 client credentials, scoped access, and JWT validation using Scalekit" browseCentral: - label: "API authentication with M2M OAuth" + label: "Secure APIs with OAuth" filterType: ["tutorial"] category: ["Auth modules"] icon: book @@ -25,9 +25,9 @@ sidebar: import { LinkButton, Aside, Steps, Tabs, TabItem, Badge } from '@astrojs/starlight/components'; import InstallSDK from '@components/templates/_installsdk.mdx'; -APIs are a way to let your customers, partners, and external systems interact with your application and it's data. It requires authentication to ensure only authorized clients can consume your APIs. Scalekit provides a way to add OAuth2.0 based client credentials authentication to your API endpoints. +APIs let your customers, partners, and external systems interact with your application and its data. You need authentication to ensure only authorized clients can consume your APIs. Scalekit helps you add OAuth 2.0-based client-credentials authentication to your API endpoints. -In should here's how it would work: +Here's how it works: ```d2 shape: sequence_diagram @@ -55,10 +55,10 @@ Your App -> API Client: 7. Returns the protected resource ```sh showLineNumbers=false pip install scalekit-sdk-python ``` - Alternatively, you can use the REST APIs directly. + Alternatively, you can use the [REST APIs directly](/apis/#tag/api-auth). @@ -368,7 +368,7 @@ Your App -> API Client: 7. Returns the protected resource Scopes are embedded in the access token and validated server-side using the Scalekit SDK. This ensures that API clients only access resources they're authorized for, adding an extra layer of security. - or example, you might create an API client for a customer's deployment service with scopes like `deploy:applications` and `read:deployments`. + For example, you might create an API client for a customer's deployment service with scopes like `deploy:applications` and `read:deployments`. diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index de461768e..b9c434c60 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -15,12 +15,12 @@ head: } --- -import { Aside, Steps, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components'; +import { Aside, Steps, Tabs, TabItem, Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; import InstallSDK from '@components/templates/_installsdk.mdx'; -When your customers need to integrate with your APIs — whether for CI/CD pipelines, partner integrations, or internal tooling — they need a straightforward way to authenticate. Scalekit API keys gives you long-lived, revocable bearer credentials that can be issued to your customers for Organizational or User Level access to your APIs. +When your customers integrate with your APIs — whether for CI/CD pipelines, partner integrations, or internal tooling — they need a straightforward way to authenticate. Scalekit API keys give you long-lived, revocable bearer credentials for organization-level or user-level access to your APIs. -Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys do not carry embedded claims and cannot be validated on the client side. The API Keys can be validated via APIs with Scalekit and after server-side validation, the claims are sent when a valid API Key is presented. Revocation of API keys takes effect immediately with no expiry windows or propagation delays. Each key is scoped to an organization and optionally to a specific user, making it easy to build personal access tokens, per-user rate limiting, and audit trails. +Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys do not carry embedded claims and cannot be validated on the client side. Validate API keys server-side through Scalekit, then use the returned claims only when the key is valid. Revocation takes effect immediately with no expiry windows or propagation delays. Each key is scoped to an organization and optionally to a specific user, making it easy to build personal access tokens, per-user rate limiting, and audit trails. In this guide, you'll learn how to create, validate, list, and revoke API keys using the Scalekit SDK. @@ -675,24 +675,28 @@ _ = userTokens // process userTokens.Tokens import com.scalekit.grpc.scalekit.v1.tokens.ListTokensResponse; import com.scalekit.grpc.scalekit.v1.tokens.Token; -// List tokens for an organization -ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); +try { + // List tokens for an organization + ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); -for (Token token : response.getTokensList()) { - System.out.println(token.getTokenId() + " " + token.getDescription()); -} + for (Token token : response.getTokensList()) { + System.out.println(token.getTokenId() + " " + token.getDescription()); + } -// Paginate through results -if (!response.getNextPageToken().isEmpty()) { - ListTokensResponse nextPage = scalekitClient.tokens().list( - organizationId, 10, response.getNextPageToken() + // Paginate through results + if (!response.getNextPageToken().isEmpty()) { + ListTokensResponse nextPage = scalekitClient.tokens().list( + organizationId, 10, response.getNextPageToken() + ); + } + + // Filter tokens by user + ListTokensResponse userTokens = scalekitClient.tokens().list( + organizationId, "usr_12345", 10, null ); +} catch (Exception e) { + System.err.println("Failed to list tokens: " + e.getMessage()); } - -// Filter tokens by user -ListTokensResponse userTokens = scalekitClient.tokens().list( - organizationId, "usr_12345", 10, null -); ``` @@ -954,4 +958,19 @@ Here are a few tips to help you get the most out of API keys in production: - **Use custom claims for context**: Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups. - **Rotate keys safely**: To rotate an API key, create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation. -You now have everything you need to issue, validate, and manage API keys in your application. \ No newline at end of file +You now have everything you need to issue, validate, and manage API keys in your application. + +## What's next + + + + + \ No newline at end of file From 9e6c4d27059fb116793ce4d260cf65a3dc0b60d2 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Wed, 25 Feb 2026 17:04:18 +0530 Subject: [PATCH 16/18] fix: redact secret in sample response and add error handling to Java list tokens example --- .../docs/authenticate/m2m/api-auth-quickstart.mdx | 2 +- src/content/docs/authenticate/m2m/api-keys.mdx | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx index 12a6a547c..d03114851 100644 --- a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx +++ b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx @@ -414,7 +414,7 @@ Your App -> API Client: 7. Returns the protected resource "read:deployments" ] }, - "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcfa6reKG1kKNVbqBKW4H5Ctmb5UZ0X" + "plain_secret": "" } ``` diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index b9c434c60..9e40dc181 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -676,26 +676,31 @@ import com.scalekit.grpc.scalekit.v1.tokens.ListTokensResponse; import com.scalekit.grpc.scalekit.v1.tokens.Token; try { - // List tokens for an organization ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); - for (Token token : response.getTokensList()) { System.out.println(token.getTokenId() + " " + token.getDescription()); } +} catch (Exception e) { + System.err.println("Failed to list tokens: " + e.getMessage()); +} - // Paginate through results +try { + ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); if (!response.getNextPageToken().isEmpty()) { ListTokensResponse nextPage = scalekitClient.tokens().list( organizationId, 10, response.getNextPageToken() ); } +} catch (Exception e) { + System.err.println("Failed to paginate tokens: " + e.getMessage()); +} - // Filter tokens by user +try { ListTokensResponse userTokens = scalekitClient.tokens().list( organizationId, "usr_12345", 10, null ); } catch (Exception e) { - System.err.println("Failed to list tokens: " + e.getMessage()); + System.err.println("Failed to list user tokens: " + e.getMessage()); } ``` From 17a184e4ca11572a04be4fa3a00c2226a5122db0 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Wed, 25 Feb 2026 17:11:03 +0530 Subject: [PATCH 17/18] fix: update sidebar label to sentence case, add space in OAuth 2.0 title, fix self-referential link in Aside --- src/content/docs/authenticate/m2m/api-auth-quickstart.mdx | 8 ++++---- src/content/docs/authenticate/m2m/api-keys.mdx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx index d03114851..0476ccc90 100644 --- a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx +++ b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx @@ -1,5 +1,5 @@ --- -title: Add OAuth2.0 to your APIs +title: Add OAuth 2.0 to your APIs description: "Secure your APIs in minutes with OAuth 2.0 client credentials, scoped access, and JWT validation using Scalekit" browseCentral: label: "Secure APIs with OAuth" @@ -516,9 +516,9 @@ Your App -> API Client: 7. Returns the protected resource Provide both values to your customer securely. Your customer will use these credentials in their application to authenticate with your API. The `plain_secret` is never shown again after creation. - + 6. ## Verify API client's scopes diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index 9e40dc181..1543ea9c1 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -2,7 +2,7 @@ title: API keys description: "Issue long-lived, revocable API keys scoped to organizations and users for programmatic access to your APIs" sidebar: - label: "Add API Keys" + label: "Add API keys" tags: [api-tokens, api-keys, m2m, authentication, bearer-tokens] head: - tag: style From faba0e2d48bda767945fbe0746bfb0c184b608a8 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Thu, 26 Feb 2026 19:26:58 +0530 Subject: [PATCH 18/18] make the org level content and user level content, even more clear --- CLAUDE.md | 14 +- .../d2/docs/authenticate/m2m/api-keys-0.svg | 336 +-- .../docs/authenticate/m2m/api-keys.mdx | 1839 +++++++++-------- 3 files changed, 1102 insertions(+), 1087 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fdcf35d86..159a75897 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,14 +10,22 @@ When in doubt, follow the constitution exactly. Do not introduce new rules that ## Core Principles -### Documentation-First Development +### Documentation-first development + +Every feature must include comprehensive, user-focused documentation. Documentation is not an afterthought but a first-class deliverable that guides implementation. All code changes require corresponding documentation updates. ### Git Commits - Do NOT include `Co-Authored-By` lines in commit messages -### SDK Variable Names (CRITICAL) -Every feature must include comprehensive, user-focused documentation. Documentation is not an afterthought but a first-class deliverable that guides implementation. All code changes require corresponding documentation updates. +### SDK variable names (critical) + +> **CRITICAL**: Use the exact variable names below in all documentation and code examples. + +- Node.js: `scalekit` +- Python: `scalekit_client` +- Go: `scalekitClient` +- Java: `scalekitClient` ### Multi-Language SDK Consistency diff --git a/public/d2/docs/authenticate/m2m/api-keys-0.svg b/public/d2/docs/authenticate/m2m/api-keys-0.svg index 306e7847f..e538bd561 100644 --- a/public/d2/docs/authenticate/m2m/api-keys-0.svg +++ b/public/d2/docs/authenticate/m2m/api-keys-0.svg @@ -1,17 +1,17 @@ - + .d2-4063762114 .fill-N1{fill:#0A0F25;} + .d2-4063762114 .fill-N2{fill:#676C7E;} + .d2-4063762114 .fill-N3{fill:#9499AB;} + .d2-4063762114 .fill-N4{fill:#CFD2DD;} + .d2-4063762114 .fill-N5{fill:#DEE1EB;} + .d2-4063762114 .fill-N6{fill:#EEF1F8;} + .d2-4063762114 .fill-N7{fill:#FFFFFF;} + .d2-4063762114 .fill-B1{fill:#0A0F25;} + .d2-4063762114 .fill-B2{fill:#676C7E;} + .d2-4063762114 .fill-B3{fill:#9499AB;} + .d2-4063762114 .fill-B4{fill:#CFD2DD;} + .d2-4063762114 .fill-B5{fill:#DEE1EB;} + .d2-4063762114 .fill-B6{fill:#EEF1F8;} + .d2-4063762114 .fill-AA2{fill:#676C7E;} + .d2-4063762114 .fill-AA4{fill:#CFD2DD;} + .d2-4063762114 .fill-AA5{fill:#DEE1EB;} + .d2-4063762114 .fill-AB4{fill:#CFD2DD;} + .d2-4063762114 .fill-AB5{fill:#DEE1EB;} + .d2-4063762114 .stroke-N1{stroke:#0A0F25;} + .d2-4063762114 .stroke-N2{stroke:#676C7E;} + .d2-4063762114 .stroke-N3{stroke:#9499AB;} + .d2-4063762114 .stroke-N4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-N5{stroke:#DEE1EB;} + .d2-4063762114 .stroke-N6{stroke:#EEF1F8;} + .d2-4063762114 .stroke-N7{stroke:#FFFFFF;} + .d2-4063762114 .stroke-B1{stroke:#0A0F25;} + .d2-4063762114 .stroke-B2{stroke:#676C7E;} + .d2-4063762114 .stroke-B3{stroke:#9499AB;} + .d2-4063762114 .stroke-B4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-B5{stroke:#DEE1EB;} + .d2-4063762114 .stroke-B6{stroke:#EEF1F8;} + .d2-4063762114 .stroke-AA2{stroke:#676C7E;} + .d2-4063762114 .stroke-AA4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-AA5{stroke:#DEE1EB;} + .d2-4063762114 .stroke-AB4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-AB5{stroke:#DEE1EB;} + .d2-4063762114 .background-color-N1{background-color:#0A0F25;} + .d2-4063762114 .background-color-N2{background-color:#676C7E;} + .d2-4063762114 .background-color-N3{background-color:#9499AB;} + .d2-4063762114 .background-color-N4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-N5{background-color:#DEE1EB;} + .d2-4063762114 .background-color-N6{background-color:#EEF1F8;} + .d2-4063762114 .background-color-N7{background-color:#FFFFFF;} + .d2-4063762114 .background-color-B1{background-color:#0A0F25;} + .d2-4063762114 .background-color-B2{background-color:#676C7E;} + .d2-4063762114 .background-color-B3{background-color:#9499AB;} + .d2-4063762114 .background-color-B4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-B5{background-color:#DEE1EB;} + .d2-4063762114 .background-color-B6{background-color:#EEF1F8;} + .d2-4063762114 .background-color-AA2{background-color:#676C7E;} + .d2-4063762114 .background-color-AA4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-AA5{background-color:#DEE1EB;} + .d2-4063762114 .background-color-AB4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-AB5{background-color:#DEE1EB;} + .d2-4063762114 .color-N1{color:#0A0F25;} + .d2-4063762114 .color-N2{color:#676C7E;} + .d2-4063762114 .color-N3{color:#9499AB;} + .d2-4063762114 .color-N4{color:#CFD2DD;} + .d2-4063762114 .color-N5{color:#DEE1EB;} + .d2-4063762114 .color-N6{color:#EEF1F8;} + .d2-4063762114 .color-N7{color:#FFFFFF;} + .d2-4063762114 .color-B1{color:#0A0F25;} + .d2-4063762114 .color-B2{color:#676C7E;} + .d2-4063762114 .color-B3{color:#9499AB;} + .d2-4063762114 .color-B4{color:#CFD2DD;} + .d2-4063762114 .color-B5{color:#DEE1EB;} + .d2-4063762114 .color-B6{color:#EEF1F8;} + .d2-4063762114 .color-AA2{color:#676C7E;} + .d2-4063762114 .color-AA4{color:#CFD2DD;} + .d2-4063762114 .color-AA5{color:#DEE1EB;} + .d2-4063762114 .color-AB4{color:#CFD2DD;} + .d2-4063762114 .color-AB5{color:#DEE1EB;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0A0F25;--color-border-muted:#676C7E;--color-neutral-muted:#EEF1F8;--color-accent-fg:#676C7E;--color-accent-emphasis:#676C7E;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-4063762114);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-dark-d2-4063762114);mix-blend-mode:overlay}.sketch-overlay-B3{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-B4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-B5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-4063762114);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-4063762114);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-4063762114);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}@media screen and (prefers-color-scheme:dark){ + .d2-4063762114 .fill-N1{fill:#0A0F25;} + .d2-4063762114 .fill-N2{fill:#676C7E;} + .d2-4063762114 .fill-N3{fill:#9499AB;} + .d2-4063762114 .fill-N4{fill:#CFD2DD;} + .d2-4063762114 .fill-N5{fill:#DEE1EB;} + .d2-4063762114 .fill-N6{fill:#EEF1F8;} + .d2-4063762114 .fill-N7{fill:#FFFFFF;} + .d2-4063762114 .fill-B1{fill:#0A0F25;} + .d2-4063762114 .fill-B2{fill:#676C7E;} + .d2-4063762114 .fill-B3{fill:#9499AB;} + .d2-4063762114 .fill-B4{fill:#CFD2DD;} + .d2-4063762114 .fill-B5{fill:#DEE1EB;} + .d2-4063762114 .fill-B6{fill:#EEF1F8;} + .d2-4063762114 .fill-AA2{fill:#676C7E;} + .d2-4063762114 .fill-AA4{fill:#CFD2DD;} + .d2-4063762114 .fill-AA5{fill:#DEE1EB;} + .d2-4063762114 .fill-AB4{fill:#CFD2DD;} + .d2-4063762114 .fill-AB5{fill:#DEE1EB;} + .d2-4063762114 .stroke-N1{stroke:#0A0F25;} + .d2-4063762114 .stroke-N2{stroke:#676C7E;} + .d2-4063762114 .stroke-N3{stroke:#9499AB;} + .d2-4063762114 .stroke-N4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-N5{stroke:#DEE1EB;} + .d2-4063762114 .stroke-N6{stroke:#EEF1F8;} + .d2-4063762114 .stroke-N7{stroke:#FFFFFF;} + .d2-4063762114 .stroke-B1{stroke:#0A0F25;} + .d2-4063762114 .stroke-B2{stroke:#676C7E;} + .d2-4063762114 .stroke-B3{stroke:#9499AB;} + .d2-4063762114 .stroke-B4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-B5{stroke:#DEE1EB;} + .d2-4063762114 .stroke-B6{stroke:#EEF1F8;} + .d2-4063762114 .stroke-AA2{stroke:#676C7E;} + .d2-4063762114 .stroke-AA4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-AA5{stroke:#DEE1EB;} + .d2-4063762114 .stroke-AB4{stroke:#CFD2DD;} + .d2-4063762114 .stroke-AB5{stroke:#DEE1EB;} + .d2-4063762114 .background-color-N1{background-color:#0A0F25;} + .d2-4063762114 .background-color-N2{background-color:#676C7E;} + .d2-4063762114 .background-color-N3{background-color:#9499AB;} + .d2-4063762114 .background-color-N4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-N5{background-color:#DEE1EB;} + .d2-4063762114 .background-color-N6{background-color:#EEF1F8;} + .d2-4063762114 .background-color-N7{background-color:#FFFFFF;} + .d2-4063762114 .background-color-B1{background-color:#0A0F25;} + .d2-4063762114 .background-color-B2{background-color:#676C7E;} + .d2-4063762114 .background-color-B3{background-color:#9499AB;} + .d2-4063762114 .background-color-B4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-B5{background-color:#DEE1EB;} + .d2-4063762114 .background-color-B6{background-color:#EEF1F8;} + .d2-4063762114 .background-color-AA2{background-color:#676C7E;} + .d2-4063762114 .background-color-AA4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-AA5{background-color:#DEE1EB;} + .d2-4063762114 .background-color-AB4{background-color:#CFD2DD;} + .d2-4063762114 .background-color-AB5{background-color:#DEE1EB;} + .d2-4063762114 .color-N1{color:#0A0F25;} + .d2-4063762114 .color-N2{color:#676C7E;} + .d2-4063762114 .color-N3{color:#9499AB;} + .d2-4063762114 .color-N4{color:#CFD2DD;} + .d2-4063762114 .color-N5{color:#DEE1EB;} + .d2-4063762114 .color-N6{color:#EEF1F8;} + .d2-4063762114 .color-N7{color:#FFFFFF;} + .d2-4063762114 .color-B1{color:#0A0F25;} + .d2-4063762114 .color-B2{color:#676C7E;} + .d2-4063762114 .color-B3{color:#9499AB;} + .d2-4063762114 .color-B4{color:#CFD2DD;} + .d2-4063762114 .color-B5{color:#DEE1EB;} + .d2-4063762114 .color-B6{color:#EEF1F8;} + .d2-4063762114 .color-AA2{color:#676C7E;} + .d2-4063762114 .color-AA4{color:#CFD2DD;} + .d2-4063762114 .color-AA5{color:#DEE1EB;} + .d2-4063762114 .color-AB4{color:#CFD2DD;} + .d2-4063762114 .color-AB5{color:#DEE1EB;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0A0F25;--color-border-muted:#676C7E;--color-neutral-muted:#EEF1F8;--color-accent-fg:#676C7E;--color-accent-emphasis:#676C7E;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-4063762114);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-dark-d2-4063762114);mix-blend-mode:overlay}.sketch-overlay-B3{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-B4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-B5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-4063762114);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-4063762114);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-4063762114);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-4063762114);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-4063762114);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}}]]> - + - + - + -API ClientUserYour AppScalekit Request API key Create token (organizationId, userId) API key + tokenId API key Configure API key Request with Authorization header Validate token Organization, user, and role info Process request Response - - - - - - - - - - - +Customer's App (API Client)UserYour AppScalekit Request API key Create token (organizationId, userId?) API key + tokenId API key Configure API key Request with Authorization header Validate token Organization ID (+ User ID if user-scoped) Filter data by org and user context Response + + + + + + + + + + + diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx index 1543ea9c1..84a6fb3c8 100644 --- a/src/content/docs/authenticate/m2m/api-keys.mdx +++ b/src/content/docs/authenticate/m2m/api-keys.mdx @@ -20,962 +20,969 @@ import InstallSDK from '@components/templates/_installsdk.mdx'; When your customers integrate with your APIs — whether for CI/CD pipelines, partner integrations, or internal tooling — they need a straightforward way to authenticate. Scalekit API keys give you long-lived, revocable bearer credentials for organization-level or user-level access to your APIs. -Unlike [JWT-based M2M authentication](/authenticate/m2m/api-auth-quickstart/), API keys do not carry embedded claims and cannot be validated on the client side. Validate API keys server-side through Scalekit, then use the returned claims only when the key is valid. Revocation takes effect immediately with no expiry windows or propagation delays. Each key is scoped to an organization and optionally to a specific user, making it easy to build personal access tokens, per-user rate limiting, and audit trails. - -In this guide, you'll learn how to create, validate, list, and revoke API keys using the Scalekit SDK. +In this guide, you'll learn how to create, validate, list, and revoke API keys using the Scalekit. ```d2 pad=36 shape: sequence_diagram -API Client +Customer's App (API Client) User Your App Scalekit User -> Your App: Request API key -Your App -> Scalekit: Create token (organizationId, userId) +Your App -> Scalekit: Create token (organizationId, userId?) Scalekit -> Your App: API key + tokenId Your App -> User: API key -User -> API Client: Configure API key -API Client -> Your App: Request with Authorization header +User -> Customer's App (API Client): Configure API key +Customer's App (API Client) -> Your App: Request with Authorization header Your App -> Scalekit: Validate token -Scalekit -> Your App: Organization, user, and role info -Your App -> Your App: Process request -Your App -> API Client: Response +Scalekit -> Your App: Organization ID (+ User ID if user-scoped) +Your App -> Your App: Filter data by org and user context +Your App -> Customer's App (API Client): Response ``` - -## Install the SDK - - - -Initialize the Scalekit client with your environment credentials: - - - - -```javascript title="Express.js" collapse={1-2} -import { ScalekitClient } from '@scalekit-sdk/node'; - -const scalekit = new ScalekitClient( - process.env.SCALEKIT_ENVIRONMENT_URL, - process.env.SCALEKIT_CLIENT_ID, - process.env.SCALEKIT_CLIENT_SECRET -); -``` - - - - -```python title="Flask" collapse={1-2} -import os -from scalekit import ScalekitClient - -scalekit_client = ScalekitClient( - env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], - client_id=os.environ["SCALEKIT_CLIENT_ID"], - client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], -) -``` - - - - -```go title="Gin" collapse={1-2} -import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2" - -scalekitClient := scalekit.NewScalekitClient( - os.Getenv("SCALEKIT_ENVIRONMENT_URL"), - os.Getenv("SCALEKIT_CLIENT_ID"), - os.Getenv("SCALEKIT_CLIENT_SECRET"), -) -``` - - - - -```java title="Spring Boot" collapse={1-2} -import com.scalekit.ScalekitClient; - -ScalekitClient scalekitClient = new ScalekitClient( - System.getenv("SCALEKIT_ENVIRONMENT_URL"), - System.getenv("SCALEKIT_CLIENT_ID"), - System.getenv("SCALEKIT_CLIENT_SECRET") -); -``` - - - - -## Create a token - -To get started, create an API key scoped to an organization. You can optionally scope it to a specific user and attach custom metadata. - -### Organization-scoped API key - -Create an API key scoped to an organization. This is the most common pattern for service-to-service integrations where the API key represents access on behalf of an entire organization. - - - - -```javascript -try { - const response = await scalekit.token.createToken(organizationId, { - description: 'CI/CD pipeline token', - }); - - // Store securely — this value cannot be retrieved again after creation - const opaqueToken = response.token; - // Stable identifier for management operations (format: apit_xxxxx) - const tokenId = response.tokenId; -} catch (error) { - console.error('Failed to create token:', error.message); -} -``` - - - - -```python -try: - response = scalekit_client.tokens.create_token( - organization_id=organization_id, - description="CI/CD pipeline token", - ) - - opaque_token = response.token # store this securely - token_id = response.token_id # format: apit_xxxxx -except Exception as e: - print(f"Failed to create token: {e}") -``` - - - - -```go -response, err := scalekitClient.Token().CreateToken( - ctx, organizationId, scalekit.CreateTokenOptions{ - Description: "CI/CD pipeline token", - }, -) -if err != nil { - log.Printf("Failed to create token: %v", err) - return -} - -// Store securely — this value cannot be retrieved again after creation -opaqueToken := response.Token -// Stable identifier for management operations (format: apit_xxxxx) -tokenId := response.TokenId -``` - - - - -```java -import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse; - -try { - CreateTokenResponse response = scalekitClient.tokens().create(organizationId); - - // Store securely — this value cannot be retrieved again after creation - String opaqueToken = response.getToken(); - // Stable identifier for management operations (format: apit_xxxxx) - String tokenId = response.getTokenId(); -} catch (Exception e) { - System.err.println("Failed to create token: " + e.getMessage()); -} -``` - - - - -### User-scoped API key - -Scope an API key to a specific user within an organization to enable personal access tokens, per-user audit trails, and user-level rate limiting. You can also attach custom claims as key-value metadata. - - - - -```javascript -try { - const userToken = await scalekit.token.createToken(organizationId, { - userId: 'usr_12345', - customClaims: { - team: 'engineering', - environment: 'production', - }, - description: 'Deployment service token', - }); - - const opaqueToken = userToken.token; - const tokenId = userToken.tokenId; -} catch (error) { - console.error('Failed to create token:', error.message); -} -``` - - - - -```python -try: - user_token = scalekit_client.tokens.create_token( - organization_id=organization_id, - user_id="usr_12345", - custom_claims={ - "team": "engineering", - "environment": "production", - }, - description="Deployment service token", - ) - - opaque_token = user_token.token - token_id = user_token.token_id -except Exception as e: - print(f"Failed to create token: {e}") -``` - - - - -```go -userToken, err := scalekitClient.Token().CreateToken( - ctx, organizationId, scalekit.CreateTokenOptions{ - UserId: "usr_12345", - CustomClaims: map[string]string{ - "team": "engineering", - "environment": "production", - }, - Description: "Deployment service token", - }, -) -if err != nil { - log.Printf("Failed to create user token: %v", err) - return -} - -opaqueToken := userToken.Token -tokenId := userToken.TokenId -``` - - - - -```java -import java.util.Map; -import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse; - -try { - Map customClaims = Map.of( - "team", "engineering", - "environment", "production" - ); - - CreateTokenResponse userToken = scalekitClient.tokens().create( - organizationId, "usr_12345", customClaims, null, "Deployment service token" - ); - - String opaqueToken = userToken.getToken(); - String tokenId = userToken.getTokenId(); -} catch (Exception e) { - System.err.println("Failed to create token: " + e.getMessage()); -} -``` - - - - -The response contains three fields: - -| Field | Description | -|-------|-------------| -| `token` | The API key string. **Returned only at creation.** | -| `token_id` | An identifier (format: `apit_xxxxx`) for referencing the token in management operations. | -| `token_info` | Metadata including organization, user, custom claims, and timestamps. | - -## Validate a token - -When your API server receives a request with an API key, you'll want to verify it's legitimate before processing the request. Pass the key to Scalekit — it validates the key server-side and returns the associated organization, user, and metadata context. - - - - -```javascript -import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; - -try { - const result = await scalekit.token.validateToken(opaqueToken); - - const orgId = result.tokenInfo?.organizationId; - const userId = result.tokenInfo?.userId; - const claims = result.tokenInfo?.customClaims; -} catch (error) { - if (error instanceof ScalekitValidateTokenFailureException) { - // Token is invalid, expired, or revoked - console.error('Token validation failed:', error.message); - } -} -``` - - - - -```python -from scalekit import ScalekitValidateTokenFailureException - -try: - result = scalekit_client.tokens.validate_token(token=opaque_token) - - org_id = result.token_info.organization_id - user_id = result.token_info.user_id - claims = result.token_info.custom_claims -except ScalekitValidateTokenFailureException: - # Token is invalid, expired, or revoked - print("Token validation failed") -``` - - - - -```go -result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) -if errors.Is(err, scalekit.ErrTokenValidationFailed) { - // Token is invalid, expired, or revoked - log.Printf("Token validation failed: %v", err) - return -} -orgId := result.TokenInfo.OrganizationId -userId := result.TokenInfo.GetUserId() // *string — nil for org-scoped tokens -claims := result.TokenInfo.CustomClaims -``` - - - - -```java -import java.util.Map; -import com.scalekit.exceptions.TokenInvalidException; -import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; - -try { - ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); - - String orgId = result.getTokenInfo().getOrganizationId(); - String userId = result.getTokenInfo().getUserId(); - Map claims = result.getTokenInfo().getCustomClaimsMap(); -} catch (TokenInvalidException e) { - // Token is invalid, expired, or revoked - System.err.println("Token validation failed: " + e.getMessage()); -} -``` - - - - -If the API key is invalid, expired, or has been revoked, validation fails with a specific error that you can catch and handle in your code. This makes it easy to reject unauthorized requests in your API middleware. - -### Access roles and organization details - -Beyond the basic organization and user information, the validation response also includes any roles assigned to the user and external identifiers you've configured for the organization. These are useful for making authorization decisions without additional database lookups. - - - - -```javascript -try { - const result = await scalekit.token.validateToken(opaqueToken); - - // Roles assigned to the user - const roles = result.tokenInfo?.roles; - - // External identifiers for mapping to your system - const externalOrgId = result.tokenInfo?.organizationExternalId; - const externalUserId = result.tokenInfo?.userExternalId; -} catch (error) { - if (error instanceof ScalekitValidateTokenFailureException) { - console.error('Token validation failed:', error.message); - } -} -``` - - - - -```python -try: - result = scalekit_client.tokens.validate_token(token=opaque_token) - - # Roles assigned to the user - roles = result.token_info.roles - - # External identifiers for mapping to your system - external_org_id = result.token_info.organization_external_id - external_user_id = result.token_info.user_external_id -except ScalekitValidateTokenFailureException: - print("Token validation failed") -``` - - - - -```go -result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) -if errors.Is(err, scalekit.ErrTokenValidationFailed) { - log.Printf("Token validation failed: %v", err) - return -} - -// Roles assigned to the user -roles := result.TokenInfo.Roles - -// External identifiers for mapping to your system -externalOrgId := result.TokenInfo.OrganizationExternalId -externalUserId := result.TokenInfo.GetUserExternalId() // *string — nil if no external ID -``` - - - - -```java -import java.util.List; -import com.scalekit.exceptions.TokenInvalidException; -import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; - -try { - ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); - - // Roles assigned to the user - List roles = result.getTokenInfo().getRolesList(); - - // External identifiers for mapping to your system - String externalOrgId = result.getTokenInfo().getOrganizationExternalId(); - String externalUserId = result.getTokenInfo().getUserExternalId(); -} catch (TokenInvalidException e) { - System.err.println("Token validation failed: " + e.getMessage()); -} -``` - - - - - -### Access custom metadata + -If you attached custom claims when creating the API key, they come back in every validation response. This is a convenient way to make fine-grained authorization decisions — like restricting access by team or environment — without hitting your database. +1. ## Install the SDK - - + -```javascript -try { - const result = await scalekit.token.validateToken(opaqueToken); + Initialize the Scalekit client with your environment credentials: - const team = result.tokenInfo?.customClaims?.team; - const environment = result.tokenInfo?.customClaims?.environment; + + - // Use metadata for authorization - if (environment !== 'production') { - return res.status(403).json({ error: 'Production access required' }); - } -} catch (error) { - if (error instanceof ScalekitValidateTokenFailureException) { - console.error('Token validation failed:', error.message); - } -} -``` + ```javascript title="Express.js" collapse={1-2} + import { ScalekitClient } from '@scalekit-sdk/node'; - - + const scalekit = new ScalekitClient( + process.env.SCALEKIT_ENVIRONMENT_URL, + process.env.SCALEKIT_CLIENT_ID, + process.env.SCALEKIT_CLIENT_SECRET + ); + ``` -```python -try: - result = scalekit_client.tokens.validate_token(token=opaque_token) + + - team = result.token_info.custom_claims.get("team") - environment = result.token_info.custom_claims.get("environment") + ```python title="Flask" collapse={1-2} + import os + from scalekit import ScalekitClient - # Use metadata for authorization - if environment != "production": - return jsonify({"error": "Production access required"}), 403 -except ScalekitValidateTokenFailureException: - print("Token validation failed") -``` - - - + scalekit_client = ScalekitClient( + env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], + client_id=os.environ["SCALEKIT_CLIENT_ID"], + client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], + ) + ``` -```go -result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) -if errors.Is(err, scalekit.ErrTokenValidationFailed) { - log.Printf("Token validation failed: %v", err) - return -} + + -team := result.TokenInfo.CustomClaims["team"] -environment := result.TokenInfo.CustomClaims["environment"] + ```go title="Gin" collapse={1-2} + import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2" -// Use metadata for authorization -if environment != "production" { - c.JSON(403, gin.H{"error": "Production access required"}) - return -} -``` + scalekitClient := scalekit.NewScalekitClient( + os.Getenv("SCALEKIT_ENVIRONMENT_URL"), + os.Getenv("SCALEKIT_CLIENT_ID"), + os.Getenv("SCALEKIT_CLIENT_SECRET"), + ) + ``` - - + + -```java -import java.util.Map; -import com.scalekit.exceptions.TokenInvalidException; -import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; + ```java title="Spring Boot" collapse={1-2} + import com.scalekit.ScalekitClient; -try { - ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + ScalekitClient scalekitClient = new ScalekitClient( + System.getenv("SCALEKIT_ENVIRONMENT_URL"), + System.getenv("SCALEKIT_CLIENT_ID"), + System.getenv("SCALEKIT_CLIENT_SECRET") + ); + ``` - String team = result.getTokenInfo().getCustomClaimsMap().get("team"); - String environment = result.getTokenInfo().getCustomClaimsMap().get("environment"); + + - // Use metadata for authorization - if (!"production".equals(environment)) { - return ResponseEntity.status(403).body(Map.of("error", "Production access required")); - } -} catch (TokenInvalidException e) { - System.err.println("Token validation failed: " + e.getMessage()); -} -``` +2. ## Create a token - - - -## List tokens - -You can retrieve all active API keys for an organization at any time. The response supports pagination for large result sets, and you can filter by user to find keys scoped to a specific person. - - - - -```javascript -try { - // List tokens for an organization - const response = await scalekit.token.listTokens(organizationId, { - pageSize: 10, - }); - - for (const token of response.tokens) { - console.log(token.tokenId, token.description); - } - - // Paginate through results - if (response.nextPageToken) { - const nextPage = await scalekit.token.listTokens(organizationId, { - pageSize: 10, - pageToken: response.nextPageToken, - }); - } - - // Filter tokens by user - const userTokens = await scalekit.token.listTokens(organizationId, { - userId: 'usr_12345', - }); -} catch (error) { - console.error('Failed to list tokens:', error.message); -} -``` - - - - -```python -try: - # List tokens for an organization - response = scalekit_client.tokens.list_tokens( - organization_id=organization_id, - page_size=10, - ) - - for token in response.tokens: - print(token.token_id, token.description) - - # Paginate through results - if response.next_page_token: - next_page = scalekit_client.tokens.list_tokens( - organization_id=organization_id, - page_size=10, - page_token=response.next_page_token, - ) - - # Filter tokens by user - user_tokens = scalekit_client.tokens.list_tokens( - organization_id=organization_id, - user_id="usr_12345", - ) -except Exception as e: - print(f"Failed to list tokens: {e}") -``` - - - - -```go -// List tokens for an organization -response, err := scalekitClient.Token().ListTokens( - ctx, organizationId, scalekit.ListTokensOptions{ - PageSize: 10, - }, -) -if err != nil { - log.Printf("Failed to list tokens: %v", err) - return -} - -for _, token := range response.Tokens { - fmt.Println(token.TokenId, token.GetDescription()) -} - -// Paginate through results -if response.NextPageToken != "" { - nextPage, err := scalekitClient.Token().ListTokens( - ctx, organizationId, scalekit.ListTokensOptions{ - PageSize: 10, - PageToken: response.NextPageToken, - }, - ) - if err != nil { - log.Printf("Failed to fetch next page: %v", err) - return - } - _ = nextPage // process nextPage.Tokens -} - -// Filter tokens by user -userTokens, err := scalekitClient.Token().ListTokens( - ctx, organizationId, scalekit.ListTokensOptions{ - UserId: "usr_12345", - }, -) -if err != nil { - log.Printf("Failed to list user tokens: %v", err) - return -} -_ = userTokens // process userTokens.Tokens -``` - - - - -```java -import com.scalekit.grpc.scalekit.v1.tokens.ListTokensResponse; -import com.scalekit.grpc.scalekit.v1.tokens.Token; - -try { - ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); - for (Token token : response.getTokensList()) { - System.out.println(token.getTokenId() + " " + token.getDescription()); - } -} catch (Exception e) { - System.err.println("Failed to list tokens: " + e.getMessage()); -} - -try { - ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); - if (!response.getNextPageToken().isEmpty()) { - ListTokensResponse nextPage = scalekitClient.tokens().list( - organizationId, 10, response.getNextPageToken() - ); - } -} catch (Exception e) { - System.err.println("Failed to paginate tokens: " + e.getMessage()); -} - -try { - ListTokensResponse userTokens = scalekitClient.tokens().list( - organizationId, "usr_12345", 10, null - ); -} catch (Exception e) { - System.err.println("Failed to list user tokens: " + e.getMessage()); -} -``` - - - - -The response includes `totalCount` for the total number of matching tokens and `nextPageToken` / `prevPageToken` cursors for navigating pages. - -## Invalidate a token - -When you need to revoke an API key — for example, when an employee leaves or you suspect credentials have been compromised — you can invalidate it through Scalekit. Revocation takes effect instantly: the very next validation request for that key will fail. - -This operation is **idempotent**, so calling invalidate on an already-revoked key succeeds without error. - - - - -```javascript -try { - // Invalidate by API key string - await scalekit.token.invalidateToken(opaqueToken); - - // Or invalidate by token_id (useful when you store tokenId for lifecycle management) - await scalekit.token.invalidateToken(tokenId); -} catch (error) { - console.error('Failed to invalidate token:', error.message); -} -``` - - - - -```python -try: - # Invalidate by API key string - scalekit_client.tokens.invalidate_token(token=opaque_token) - - # Or invalidate by token_id (useful when you store token_id for lifecycle management) - scalekit_client.tokens.invalidate_token(token=token_id) -except Exception as e: - print(f"Failed to invalidate token: {e}") -``` - - - - -```go -// Invalidate by API key string -if err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken); err != nil { - log.Printf("Failed to invalidate token: %v", err) -} - -// Or invalidate by token_id (useful when you store tokenId for lifecycle management) -if err := scalekitClient.Token().InvalidateToken(ctx, tokenId); err != nil { - log.Printf("Failed to invalidate token: %v", err) -} -``` - - - - -```java -try { - // Invalidate by API key string - scalekitClient.tokens().invalidate(opaqueToken); - - // Or invalidate by token_id (useful when you store tokenId for lifecycle management) - scalekitClient.tokens().invalidate(tokenId); -} catch (Exception e) { - System.err.println("Failed to invalidate token: " + e.getMessage()); -} -``` - - - - -## Protect your API endpoints - -Now let's put it all together. The most common pattern is to add API key validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned context for authorization decisions. - - - - -```javascript title="Express.js" -import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; - -async function authenticateToken(req, res, next) { - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; - - if (!token) { - // Reject requests without credentials to prevent unauthorized access - return res.status(401).json({ error: 'Missing authorization token' }); - } - - try { - // Server-side validation — Scalekit checks token status in real time - const result = await scalekit.token.validateToken(token); - // Attach token context to the request for downstream handlers - req.tokenInfo = result.tokenInfo; - next(); - } catch (error) { - if (error instanceof ScalekitValidateTokenFailureException) { - // Revoked, expired, or malformed tokens are rejected immediately - return res.status(401).json({ error: 'Invalid or expired token' }); - } - throw error; - } -} - -// Apply to protected routes -app.get('/api/resources', authenticateToken, (req, res) => { - const orgId = req.tokenInfo.organizationId; - // Serve resources scoped to this organization -}); -``` - - - - -```python title="Flask" -from functools import wraps -from flask import request, jsonify, g -from scalekit import ScalekitValidateTokenFailureException - -def authenticate_token(f): - @wraps(f) - def decorated(*args, **kwargs): - auth_header = request.headers.get("Authorization", "") - if not auth_header.startswith("Bearer "): - # Reject requests without credentials to prevent unauthorized access - return jsonify({"error": "Missing authorization token"}), 401 - - token = auth_header.split(" ")[1] - - try: - # Server-side validation — Scalekit checks token status in real time - result = scalekit_client.tokens.validate_token(token=token) - # Attach token context for downstream handlers - g.token_info = result.token_info - except ScalekitValidateTokenFailureException: - # Revoked, expired, or malformed tokens are rejected immediately - return jsonify({"error": "Invalid or expired token"}), 401 - - return f(*args, **kwargs) - return decorated - -# Apply to protected routes -@app.route("/api/resources") -@authenticate_token -def get_resources(): - org_id = g.token_info.organization_id - # Serve resources scoped to this organization -``` - - - - -```go title="Gin" -func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc { - return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if !strings.HasPrefix(authHeader, "Bearer ") { - // Reject requests without credentials to prevent unauthorized access - c.JSON(401, gin.H{"error": "Missing authorization token"}) - c.Abort() - return - } - - token := strings.TrimPrefix(authHeader, "Bearer ") - - // Server-side validation — Scalekit checks token status in real time - result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token) - if err != nil { - if errors.Is(err, scalekit.ErrTokenValidationFailed) { - // Revoked, expired, or malformed tokens are rejected immediately - c.JSON(401, gin.H{"error": "Invalid or expired token"}) - } else { - // Surface transport or unexpected errors as 500 - c.JSON(500, gin.H{"error": "Internal server error"}) - } - c.Abort() - return - } - - // Attach token context for downstream handlers - c.Set("tokenInfo", result.TokenInfo) - c.Next() - } -} - -// Apply to protected routes -r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) { - tokenInfo := c.MustGet("tokenInfo").(*scalekit.TokenInfo) - orgId := tokenInfo.OrganizationId - // Serve resources scoped to this organization -}) -``` - - - - -```java title="Spring Boot" -import com.scalekit.exceptions.TokenInvalidException; -import com.scalekit.grpc.scalekit.v1.tokens.Token; -import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; - -@Component -public class TokenAuthFilter extends OncePerRequestFilter { - private final ScalekitClient scalekitClient; - - public TokenAuthFilter(ScalekitClient scalekitClient) { - this.scalekitClient = scalekitClient; - } - - @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain - ) throws ServletException, IOException { - String authHeader = request.getHeader("Authorization"); - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - // Reject requests without credentials to prevent unauthorized access - response.sendError(401, "Missing authorization token"); - return; - } - - String token = authHeader.substring(7); - - try { - // Server-side validation — Scalekit checks token status in real time - ValidateTokenResponse result = scalekitClient.tokens().validate(token); - // Attach token context for downstream handlers - request.setAttribute("tokenInfo", result.getTokenInfo()); - filterChain.doFilter(request, response); - } catch (TokenInvalidException e) { - // Revoked, expired, or malformed tokens are rejected immediately - response.sendError(401, "Invalid or expired token"); - } - } -} - -// Access in your controller -@GetMapping("/api/resources") -public ResponseEntity getResources(HttpServletRequest request) { - Token tokenInfo = (Token) request.getAttribute("tokenInfo"); - String orgId = tokenInfo.getOrganizationId(); - // Serve resources scoped to this organization -} -``` + To get started, create an API key scoped to an organization. You can optionally scope it to a specific user and attach custom metadata. - - + ### Organization-scoped API key -Here are a few tips to help you get the most out of API keys in production: + **When to use**: Organization-scoped keys are for customers who need full access to all resources within their workspace or account. When they authenticate with the key, Scalekit validates it and confirms the organization context — your API then exposes all resources they own. -- **Store API keys securely**: Treat API keys like passwords. Store them in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code. -- **Set expiry for time-limited access**: Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised. -- **Use custom claims for context**: Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups. -- **Rotate keys safely**: To rotate an API key, create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation. + **Example scenario**: You're building a CRM like HubSpot. Your customer integrates with your API using an organization-scoped key. When they request contacts, tasks, or deals, the key validates successfully for their organization, and your API returns all resources in that workspace. -You now have everything you need to issue, validate, and manage API keys in your application. + This is the most common pattern for service-to-service integrations where the API key represents access on behalf of an entire organization. -## What's next + + + + ```javascript + try { + const response = await scalekit.token.createToken(organizationId, { + description: 'CI/CD pipeline token', + }); - - - - \ No newline at end of file + // Store securely — this value cannot be retrieved again after creation + const opaqueToken = response.token; + // Stable identifier for management operations (format: apit_xxxxx) + const tokenId = response.tokenId; + } catch (error) { + console.error('Failed to create token:', error.message); + } + ``` + + + + + ```python + try: + response = scalekit_client.tokens.create_token( + organization_id=organization_id, + description="CI/CD pipeline token", + ) + + opaque_token = response.token # store this securely + token_id = response.token_id # format: apit_xxxxx + except Exception as e: + print(f"Failed to create token: {e}") + ``` + + + + + ```go + response, err := scalekitClient.Token().CreateToken( + ctx, organizationId, scalekit.CreateTokenOptions{ + Description: "CI/CD pipeline token", + }, + ) + if err != nil { + log.Printf("Failed to create token: %v", err) + return + } + + // Store securely — this value cannot be retrieved again after creation + opaqueToken := response.Token + // Stable identifier for management operations (format: apit_xxxxx) + tokenId := response.TokenId + ``` + + + + + ```java + import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse; + + try { + CreateTokenResponse response = scalekitClient.tokens().create(organizationId); + + // Store securely — this value cannot be retrieved again after creation + String opaqueToken = response.getToken(); + // Stable identifier for management operations (format: apit_xxxxx) + String tokenId = response.getTokenId(); + } catch (Exception e) { + System.err.println("Failed to create token: " + e.getMessage()); + } + ``` + + + + + ### User-scoped API key + + **When to use**: User-scoped keys enable fine-grained data filtering based on who owns the key. Your API validates the key, receives the user context, and then exposes only data relevant to that user — enabling role-based filtering without additional database lookups. + + **Example scenario**: Your CRM has a `/tasks` endpoint. One customer gives their team member a user-scoped API key. When that person calls `/tasks`, the key validates for their organization _and_ user, and your API returns only tasks assigned to them — not all tasks in the workspace. Another team member with a different key sees only their own tasks. + + User-scoped keys enable personal access tokens, per-user audit trails, and user-level rate limiting. You can also attach custom claims as key-value metadata. + + + + + ```javascript + try { + const userToken = await scalekit.token.createToken(organizationId, { + userId: 'usr_12345', + customClaims: { + team: 'engineering', + environment: 'production', + }, + description: 'Deployment service token', + }); + + const opaqueToken = userToken.token; + const tokenId = userToken.tokenId; + } catch (error) { + console.error('Failed to create token:', error.message); + } + ``` + + + + + ```python + try: + user_token = scalekit_client.tokens.create_token( + organization_id=organization_id, + user_id="usr_12345", + custom_claims={ + "team": "engineering", + "environment": "production", + }, + description="Deployment service token", + ) + + opaque_token = user_token.token + token_id = user_token.token_id + except Exception as e: + print(f"Failed to create token: {e}") + ``` + + + + + ```go + userToken, err := scalekitClient.Token().CreateToken( + ctx, organizationId, scalekit.CreateTokenOptions{ + UserId: "usr_12345", + CustomClaims: map[string]string{ + "team": "engineering", + "environment": "production", + }, + Description: "Deployment service token", + }, + ) + if err != nil { + log.Printf("Failed to create user token: %v", err) + return + } + + opaqueToken := userToken.Token + tokenId := userToken.TokenId + ``` + + + + + ```java + import java.util.Map; + import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse; + + try { + Map customClaims = Map.of( + "team", "engineering", + "environment", "production" + ); + + CreateTokenResponse userToken = scalekitClient.tokens().create( + organizationId, "usr_12345", customClaims, null, "Deployment service token" + ); + + String opaqueToken = userToken.getToken(); + String tokenId = userToken.getTokenId(); + } catch (Exception e) { + System.err.println("Failed to create token: " + e.getMessage()); + } + ``` + + + + + The response contains three fields: + + | Field | Description | + |-------|-------------| + | `token` | The API key string. **Returned only at creation.** | + | `token_id` | An identifier (format: `apit_xxxxx`) for referencing the token in management operations. | + | `token_info` | Metadata including organization, user, custom claims, and timestamps. | + +3. ## Validate a token + + When your API server receives a request with an API key, you'll want to verify it's legitimate before processing the request. Pass the key to Scalekit — it validates the key server-side and returns the associated organization, user, and metadata context. + + + + + ```javascript + import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; + + try { + const result = await scalekit.token.validateToken(opaqueToken); + + const orgId = result.tokenInfo?.organizationId; + const userId = result.tokenInfo?.userId; + const claims = result.tokenInfo?.customClaims; + } catch (error) { + if (error instanceof ScalekitValidateTokenFailureException) { + // Token is invalid, expired, or revoked + console.error('Token validation failed:', error.message); + } + } + ``` + + + + + ```python + from scalekit import ScalekitValidateTokenFailureException + + try: + result = scalekit_client.tokens.validate_token(token=opaque_token) + + org_id = result.token_info.organization_id + user_id = result.token_info.user_id + claims = result.token_info.custom_claims + except ScalekitValidateTokenFailureException: + # Token is invalid, expired, or revoked + print("Token validation failed") + ``` + + + + + ```go + result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) + if errors.Is(err, scalekit.ErrTokenValidationFailed) { + // Token is invalid, expired, or revoked + log.Printf("Token validation failed: %v", err) + return + } + + orgId := result.TokenInfo.OrganizationId + userId := result.TokenInfo.GetUserId() // *string — nil for org-scoped tokens + claims := result.TokenInfo.CustomClaims + ``` + + + + + ```java + import java.util.Map; + import com.scalekit.exceptions.TokenInvalidException; + import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; + + try { + ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + + String orgId = result.getTokenInfo().getOrganizationId(); + String userId = result.getTokenInfo().getUserId(); + Map claims = result.getTokenInfo().getCustomClaimsMap(); + } catch (TokenInvalidException e) { + // Token is invalid, expired, or revoked + System.err.println("Token validation failed: " + e.getMessage()); + } + ``` + + + + + If the API key is invalid, expired, or has been revoked, validation fails with a specific error that you can catch and handle in your code. This makes it easy to reject unauthorized requests in your API middleware. + + ### Access roles and organization details + + Beyond the basic organization and user information, the validation response also includes any roles assigned to the user and external identifiers you've configured for the organization. These are useful for making authorization decisions without additional database lookups. + + + + + ```javascript + try { + const result = await scalekit.token.validateToken(opaqueToken); + + // Roles assigned to the user + const roles = result.tokenInfo?.roles; + + // External identifiers for mapping to your system + const externalOrgId = result.tokenInfo?.organizationExternalId; + const externalUserId = result.tokenInfo?.userExternalId; + } catch (error) { + if (error instanceof ScalekitValidateTokenFailureException) { + console.error('Token validation failed:', error.message); + } + } + ``` + + + + + ```python + try: + result = scalekit_client.tokens.validate_token(token=opaque_token) + + # Roles assigned to the user + roles = result.token_info.roles + + # External identifiers for mapping to your system + external_org_id = result.token_info.organization_external_id + external_user_id = result.token_info.user_external_id + except ScalekitValidateTokenFailureException: + print("Token validation failed") + ``` + + + + + ```go + result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) + if errors.Is(err, scalekit.ErrTokenValidationFailed) { + log.Printf("Token validation failed: %v", err) + return + } + + // Roles assigned to the user + roles := result.TokenInfo.Roles + + // External identifiers for mapping to your system + externalOrgId := result.TokenInfo.OrganizationExternalId + externalUserId := result.TokenInfo.GetUserExternalId() // *string — nil if no external ID + ``` + + + + + ```java + import java.util.List; + import com.scalekit.exceptions.TokenInvalidException; + import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; + + try { + ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + + // Roles assigned to the user + List roles = result.getTokenInfo().getRolesList(); + + // External identifiers for mapping to your system + String externalOrgId = result.getTokenInfo().getOrganizationExternalId(); + String externalUserId = result.getTokenInfo().getUserExternalId(); + } catch (TokenInvalidException e) { + System.err.println("Token validation failed: " + e.getMessage()); + } + ``` + + + + + + + ### Access custom metadata + + If you attached custom claims when creating the API key, they come back in every validation response. This is a convenient way to make fine-grained authorization decisions — like restricting access by team or environment — without hitting your database. + + + + + ```javascript + try { + const result = await scalekit.token.validateToken(opaqueToken); + + const team = result.tokenInfo?.customClaims?.team; + const environment = result.tokenInfo?.customClaims?.environment; + + // Use metadata for authorization + if (environment !== 'production') { + return res.status(403).json({ error: 'Production access required' }); + } + } catch (error) { + if (error instanceof ScalekitValidateTokenFailureException) { + console.error('Token validation failed:', error.message); + } + } + ``` + + + + + ```python + try: + result = scalekit_client.tokens.validate_token(token=opaque_token) + + team = result.token_info.custom_claims.get("team") + environment = result.token_info.custom_claims.get("environment") + + # Use metadata for authorization + if environment != "production": + return jsonify({"error": "Production access required"}), 403 + except ScalekitValidateTokenFailureException: + print("Token validation failed") + ``` + + + + + ```go + result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) + if errors.Is(err, scalekit.ErrTokenValidationFailed) { + log.Printf("Token validation failed: %v", err) + return + } + + team := result.TokenInfo.CustomClaims["team"] + environment := result.TokenInfo.CustomClaims["environment"] + + // Use metadata for authorization + if environment != "production" { + c.JSON(403, gin.H{"error": "Production access required"}) + return + } + ``` + + + + + ```java + import java.util.Map; + import com.scalekit.exceptions.TokenInvalidException; + import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; + + try { + ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken); + + String team = result.getTokenInfo().getCustomClaimsMap().get("team"); + String environment = result.getTokenInfo().getCustomClaimsMap().get("environment"); + + // Use metadata for authorization + if (!"production".equals(environment)) { + return ResponseEntity.status(403).body(Map.of("error", "Production access required")); + } + } catch (TokenInvalidException e) { + System.err.println("Token validation failed: " + e.getMessage()); + } + ``` + + + + +4. ## List tokens + + You can retrieve all active API keys for an organization at any time. The response supports pagination for large result sets, and you can filter by user to find keys scoped to a specific person. + + + + + ```javascript + try { + // List tokens for an organization + const response = await scalekit.token.listTokens(organizationId, { + pageSize: 10, + }); + + for (const token of response.tokens) { + console.log(token.tokenId, token.description); + } + + // Paginate through results + if (response.nextPageToken) { + const nextPage = await scalekit.token.listTokens(organizationId, { + pageSize: 10, + pageToken: response.nextPageToken, + }); + } + + // Filter tokens by user + const userTokens = await scalekit.token.listTokens(organizationId, { + userId: 'usr_12345', + }); + } catch (error) { + console.error('Failed to list tokens:', error.message); + } + ``` + + + + + ```python + try: + # List tokens for an organization + response = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + page_size=10, + ) + + for token in response.tokens: + print(token.token_id, token.description) + + # Paginate through results + if response.next_page_token: + next_page = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + page_size=10, + page_token=response.next_page_token, + ) + + # Filter tokens by user + user_tokens = scalekit_client.tokens.list_tokens( + organization_id=organization_id, + user_id="usr_12345", + ) + except Exception as e: + print(f"Failed to list tokens: {e}") + ``` + + + + + ```go + // List tokens for an organization + response, err := scalekitClient.Token().ListTokens( + ctx, organizationId, scalekit.ListTokensOptions{ + PageSize: 10, + }, + ) + if err != nil { + log.Printf("Failed to list tokens: %v", err) + return + } + + for _, token := range response.Tokens { + fmt.Println(token.TokenId, token.GetDescription()) + } + + // Paginate through results + if response.NextPageToken != "" { + nextPage, err := scalekitClient.Token().ListTokens( + ctx, organizationId, scalekit.ListTokensOptions{ + PageSize: 10, + PageToken: response.NextPageToken, + }, + ) + if err != nil { + log.Printf("Failed to fetch next page: %v", err) + return + } + _ = nextPage // process nextPage.Tokens + } + + // Filter tokens by user + userTokens, err := scalekitClient.Token().ListTokens( + ctx, organizationId, scalekit.ListTokensOptions{ + UserId: "usr_12345", + }, + ) + if err != nil { + log.Printf("Failed to list user tokens: %v", err) + return + } + _ = userTokens // process userTokens.Tokens + ``` + + + + + ```java + import com.scalekit.grpc.scalekit.v1.tokens.ListTokensResponse; + import com.scalekit.grpc.scalekit.v1.tokens.Token; + + try { + ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); + for (Token token : response.getTokensList()) { + System.out.println(token.getTokenId() + " " + token.getDescription()); + } + } catch (Exception e) { + System.err.println("Failed to list tokens: " + e.getMessage()); + } + + try { + ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null); + if (!response.getNextPageToken().isEmpty()) { + ListTokensResponse nextPage = scalekitClient.tokens().list( + organizationId, 10, response.getNextPageToken() + ); + } + } catch (Exception e) { + System.err.println("Failed to paginate tokens: " + e.getMessage()); + } + + try { + ListTokensResponse userTokens = scalekitClient.tokens().list( + organizationId, "usr_12345", 10, null + ); + } catch (Exception e) { + System.err.println("Failed to list user tokens: " + e.getMessage()); + } + ``` + + + + + The response includes `totalCount` for the total number of matching tokens and `nextPageToken` / `prevPageToken` cursors for navigating pages. + +5. ## Invalidate a token + + When you need to revoke an API key — for example, when an employee leaves or you suspect credentials have been compromised — you can invalidate it through Scalekit. Revocation takes effect instantly: the very next validation request for that key will fail. + + This operation is **idempotent**, so calling invalidate on an already-revoked key succeeds without error. + + + + + ```javascript + try { + // Invalidate by API key string + await scalekit.token.invalidateToken(opaqueToken); + + // Or invalidate by token_id (useful when you store tokenId for lifecycle management) + await scalekit.token.invalidateToken(tokenId); + } catch (error) { + console.error('Failed to invalidate token:', error.message); + } + ``` + + + + + ```python + try: + # Invalidate by API key string + scalekit_client.tokens.invalidate_token(token=opaque_token) + + # Or invalidate by token_id (useful when you store token_id for lifecycle management) + scalekit_client.tokens.invalidate_token(token=token_id) + except Exception as e: + print(f"Failed to invalidate token: {e}") + ``` + + + + + ```go + // Invalidate by API key string + if err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken); err != nil { + log.Printf("Failed to invalidate token: %v", err) + } + + // Or invalidate by token_id (useful when you store tokenId for lifecycle management) + if err := scalekitClient.Token().InvalidateToken(ctx, tokenId); err != nil { + log.Printf("Failed to invalidate token: %v", err) + } + ``` + + + + + ```java + try { + // Invalidate by API key string + scalekitClient.tokens().invalidate(opaqueToken); + + // Or invalidate by token_id (useful when you store tokenId for lifecycle management) + scalekitClient.tokens().invalidate(tokenId); + } catch (Exception e) { + System.err.println("Failed to invalidate token: " + e.getMessage()); + } + ``` + + + + +6. ## Protect your API endpoints + + Now let's put it all together. The most common pattern is to add API key validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned context for authorization decisions. + + + + + ```javascript title="Express.js" + import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; + + async function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + // Reject requests without credentials to prevent unauthorized access + return res.status(401).json({ error: 'Missing authorization token' }); + } + + try { + // Server-side validation — Scalekit checks token status in real time + const result = await scalekit.token.validateToken(token); + // Attach token context to the request for downstream handlers + req.tokenInfo = result.tokenInfo; + next(); + } catch (error) { + if (error instanceof ScalekitValidateTokenFailureException) { + // Revoked, expired, or malformed tokens are rejected immediately + return res.status(401).json({ error: 'Invalid or expired token' }); + } + throw error; + } + } + + // Apply to protected routes + app.get('/api/resources', authenticateToken, (req, res) => { + const orgId = req.tokenInfo.organizationId; + // Serve resources scoped to this organization + }); + ``` + + + + + ```python title="Flask" + from functools import wraps + from flask import request, jsonify, g + from scalekit import ScalekitValidateTokenFailureException + + def authenticate_token(f): + @wraps(f) + def decorated(*args, **kwargs): + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + # Reject requests without credentials to prevent unauthorized access + return jsonify({"error": "Missing authorization token"}), 401 + + token = auth_header.split(" ")[1] + + try: + # Server-side validation — Scalekit checks token status in real time + result = scalekit_client.tokens.validate_token(token=token) + # Attach token context for downstream handlers + g.token_info = result.token_info + except ScalekitValidateTokenFailureException: + # Revoked, expired, or malformed tokens are rejected immediately + return jsonify({"error": "Invalid or expired token"}), 401 + + return f(*args, **kwargs) + return decorated + + # Apply to protected routes + @app.route("/api/resources") + @authenticate_token + def get_resources(): + org_id = g.token_info.organization_id + # Serve resources scoped to this organization + ``` + + + + + ```go title="Gin" + func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if !strings.HasPrefix(authHeader, "Bearer ") { + // Reject requests without credentials to prevent unauthorized access + c.JSON(401, gin.H{"error": "Missing authorization token"}) + c.Abort() + return + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + + // Server-side validation — Scalekit checks token status in real time + result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token) + if err != nil { + if errors.Is(err, scalekit.ErrTokenValidationFailed) { + // Revoked, expired, or malformed tokens are rejected immediately + c.JSON(401, gin.H{"error": "Invalid or expired token"}) + } else { + // Surface transport or unexpected errors as 500 + c.JSON(500, gin.H{"error": "Internal server error"}) + } + c.Abort() + return + } + + // Attach token context for downstream handlers + c.Set("tokenInfo", result.TokenInfo) + c.Next() + } + } + + // Apply to protected routes + r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) { + tokenInfo := c.MustGet("tokenInfo").(*scalekit.TokenInfo) + orgId := tokenInfo.OrganizationId + // Serve resources scoped to this organization + }) + ``` + + + + + ```java title="Spring Boot" + import com.scalekit.exceptions.TokenInvalidException; + import com.scalekit.grpc.scalekit.v1.tokens.Token; + import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse; + + @Component + public class TokenAuthFilter extends OncePerRequestFilter { + private final ScalekitClient scalekitClient; + + public TokenAuthFilter(ScalekitClient scalekitClient) { + this.scalekitClient = scalekitClient; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + // Reject requests without credentials to prevent unauthorized access + response.sendError(401, "Missing authorization token"); + return; + } + + String token = authHeader.substring(7); + + try { + // Server-side validation — Scalekit checks token status in real time + ValidateTokenResponse result = scalekitClient.tokens().validate(token); + // Attach token context for downstream handlers + request.setAttribute("tokenInfo", result.getTokenInfo()); + filterChain.doFilter(request, response); + } catch (TokenInvalidException e) { + // Revoked, expired, or malformed tokens are rejected immediately + response.sendError(401, "Invalid or expired token"); + } + } + } + + // Access in your controller + @GetMapping("/api/resources") + public ResponseEntity getResources(HttpServletRequest request) { + Token tokenInfo = (Token) request.getAttribute("tokenInfo"); + String orgId = tokenInfo.getOrganizationId(); + // Serve resources scoped to this organization + } + ``` + + + + + ### Using validation context for data filtering + + After validation succeeds, your middleware has access to the organization and (optionally) user context. Use this context to filter the data your endpoint returns — no additional database queries needed. + + **For organization-scoped keys**: Extract the organization ID from the validation response. Your endpoint then returns resources belonging to that organization. If a customer authenticates with an organization-scoped key, they get access to all their workspace data. + + **For user-scoped keys**: Extract both organization ID and user ID. Filter your query to return only resources belonging to that user within the organization. If a team member authenticates with a user-scoped key, they see only their assigned tasks, their owned projects, or their allocated resources — depending on your application logic. + + The validation response is your source of truth. Trust the organization and user context it provides, and use it to build your authorization queries without additional lookups. + + Here are a few tips to help you get the most out of API keys in production: + + - **Store API keys securely**: Treat API keys like passwords. Store them in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code. + - **Set expiry for time-limited access**: Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised. + - **Use custom claims for context**: Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups. + - **Rotate keys safely**: To rotate an API key, create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation. + + You now have everything you need to issue, validate, and manage API keys in your application. + + \ No newline at end of file