@mastra/auth-okta 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +204 -0
- package/dist/auth-provider.d.ts +120 -0
- package/dist/auth-provider.d.ts.map +1 -0
- package/dist/index.cjs +593 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +585 -0
- package/dist/index.js.map +1 -0
- package/dist/rbac-provider.d.ts +134 -0
- package/dist/rbac-provider.d.ts.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +65 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# @mastra/auth-okta
|
|
2
|
+
|
|
3
|
+
Okta integration for Mastra, providing RBAC based on Okta groups and optional JWT authentication.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 **RBAC Provider** - Map Okta groups to Mastra permissions
|
|
8
|
+
- 🔑 **Auth Provider** - JWT token verification using Okta's JWKS
|
|
9
|
+
- 🔄 **Cross-provider support** - Use with Auth0, Clerk, or any other auth provider
|
|
10
|
+
- ⚡ **Caching** - LRU cache for group lookups to minimize API calls
|
|
11
|
+
- 🛡️ **Type-safe** - Full TypeScript support with strict types
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @mastra/auth-okta
|
|
17
|
+
# or
|
|
18
|
+
yarn add @mastra/auth-okta
|
|
19
|
+
# or
|
|
20
|
+
pnpm add @mastra/auth-okta
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Auth0 + Okta RBAC (Recommended)
|
|
26
|
+
|
|
27
|
+
Use Auth0 for authentication and Okta for role-based access control:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Mastra } from '@mastra/core/mastra';
|
|
31
|
+
import { MastraAuthAuth0 } from '@mastra/auth-auth0';
|
|
32
|
+
import { MastraRBACOkta } from '@mastra/auth-okta';
|
|
33
|
+
|
|
34
|
+
const mastra = new Mastra({
|
|
35
|
+
server: {
|
|
36
|
+
auth: new MastraAuthAuth0(),
|
|
37
|
+
rbac: new MastraRBACOkta({
|
|
38
|
+
// Extract Okta user ID from Auth0 user metadata
|
|
39
|
+
getUserId: user => user.metadata?.oktaUserId || user.email,
|
|
40
|
+
roleMapping: {
|
|
41
|
+
Engineering: ['agents:*', 'workflows:*'],
|
|
42
|
+
Product: ['agents:read', 'workflows:read'],
|
|
43
|
+
Admin: ['*'],
|
|
44
|
+
_default: [], // Users with unmapped groups get no permissions
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Full Okta Setup
|
|
52
|
+
|
|
53
|
+
For complete Okta authentication + RBAC:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Mastra } from '@mastra/core/mastra';
|
|
57
|
+
import { MastraAuthOkta, MastraRBACOkta } from '@mastra/auth-okta';
|
|
58
|
+
|
|
59
|
+
const mastra = new Mastra({
|
|
60
|
+
server: {
|
|
61
|
+
auth: new MastraAuthOkta(),
|
|
62
|
+
rbac: new MastraRBACOkta({
|
|
63
|
+
roleMapping: {
|
|
64
|
+
Admin: ['*'],
|
|
65
|
+
Member: ['agents:read', 'workflows:*'],
|
|
66
|
+
_default: [],
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
### Environment Variables
|
|
76
|
+
|
|
77
|
+
| Variable | Description | Required |
|
|
78
|
+
| ---------------------- | ----------------------------------------- | -------------------------------------------------- |
|
|
79
|
+
| `OKTA_DOMAIN` | Okta domain (e.g., `dev-123456.okta.com`) | Yes |
|
|
80
|
+
| `OKTA_CLIENT_ID` | OAuth client ID | For auth only |
|
|
81
|
+
| `OKTA_CLIENT_SECRET` | OAuth client secret | For auth only |
|
|
82
|
+
| `OKTA_REDIRECT_URI` | OAuth redirect URI for SSO callback | For auth only |
|
|
83
|
+
| `OKTA_ISSUER` | Token issuer URL | No (defaults to `https://{domain}/oauth2/default`) |
|
|
84
|
+
| `OKTA_COOKIE_PASSWORD` | Session encryption key (min 32 chars) | No (auto-generated if omitted; set for production) |
|
|
85
|
+
| `OKTA_API_TOKEN` | API token for management SDK | For RBAC only |
|
|
86
|
+
|
|
87
|
+
### MastraAuthOkta Options
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
interface MastraAuthOktaOptions {
|
|
91
|
+
domain?: string; // Okta domain (or OKTA_DOMAIN env var)
|
|
92
|
+
clientId?: string; // OAuth client ID (or OKTA_CLIENT_ID)
|
|
93
|
+
clientSecret?: string; // OAuth client secret (or OKTA_CLIENT_SECRET)
|
|
94
|
+
redirectUri?: string; // SSO callback URI (or OKTA_REDIRECT_URI)
|
|
95
|
+
issuer?: string; // Token issuer URL (or OKTA_ISSUER)
|
|
96
|
+
scopes?: string[]; // OAuth scopes (default: ['openid', 'profile', 'email', 'groups'])
|
|
97
|
+
name?: string; // Provider name (default: 'okta')
|
|
98
|
+
session?: {
|
|
99
|
+
cookieName?: string; // Cookie name (default: 'okta_session')
|
|
100
|
+
cookieMaxAge?: number; // Cookie max age in seconds (default: 86400)
|
|
101
|
+
cookiePassword?: string; // Encryption key, min 32 chars (or OKTA_COOKIE_PASSWORD)
|
|
102
|
+
secureCookies?: boolean; // Set Secure flag (default: auto-detect from NODE_ENV)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### MastraRBACOkta Options
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface MastraRBACOktaOptions {
|
|
111
|
+
domain?: string; // Okta domain
|
|
112
|
+
apiToken?: string; // API token for group fetching
|
|
113
|
+
|
|
114
|
+
// Map Okta groups to Mastra permissions
|
|
115
|
+
roleMapping: {
|
|
116
|
+
[groupName: string]: string[]; // e.g., 'Admin': ['*']
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Extract Okta user ID from any user object (for cross-provider use)
|
|
120
|
+
getUserId?: (user: unknown) => string | undefined;
|
|
121
|
+
|
|
122
|
+
// Cache configuration
|
|
123
|
+
cache?: {
|
|
124
|
+
maxSize?: number; // Max users to cache (default: 1000)
|
|
125
|
+
ttlMs?: number; // Cache TTL in ms (default: 60000)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Role Mapping
|
|
131
|
+
|
|
132
|
+
The `roleMapping` configuration maps Okta group names to Mastra permission patterns:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
roleMapping: {
|
|
136
|
+
// Users in 'Engineering' group get full access to agents and workflows
|
|
137
|
+
'Engineering': ['agents:*', 'workflows:*'],
|
|
138
|
+
|
|
139
|
+
// Users in 'Product' group get read-only access
|
|
140
|
+
'Product': ['agents:read', 'workflows:read'],
|
|
141
|
+
|
|
142
|
+
// Users in 'Admin' group get full access to everything
|
|
143
|
+
'Admin': ['*'],
|
|
144
|
+
|
|
145
|
+
// Special '_default' key: permissions for users with unmapped groups
|
|
146
|
+
'_default': [],
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Permission Patterns
|
|
151
|
+
|
|
152
|
+
- `*` - Full access to all resources
|
|
153
|
+
- `agents:*` - Full access to agents
|
|
154
|
+
- `agents:read` - Read-only access to agents
|
|
155
|
+
- `workflows:create` - Create workflows only
|
|
156
|
+
|
|
157
|
+
## Cross-Provider Support
|
|
158
|
+
|
|
159
|
+
When using a different auth provider (Auth0, Clerk, etc.) with Okta RBAC, you need to link users between systems. The `getUserId` option allows you to extract the Okta user ID from any user object:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
new MastraRBACOkta({
|
|
163
|
+
// Extract Okta user ID from Auth0 user's app_metadata
|
|
164
|
+
getUserId: (user) => {
|
|
165
|
+
return user.metadata?.oktaUserId || user.email;
|
|
166
|
+
},
|
|
167
|
+
roleMapping: { ... },
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Linking Users
|
|
172
|
+
|
|
173
|
+
To link Auth0 users to Okta:
|
|
174
|
+
|
|
175
|
+
1. Store the Okta user ID in Auth0's `app_metadata`
|
|
176
|
+
2. Configure `getUserId` to extract it
|
|
177
|
+
3. Mastra will use this ID to fetch groups from Okta
|
|
178
|
+
|
|
179
|
+
## API
|
|
180
|
+
|
|
181
|
+
### MastraRBACOkta
|
|
182
|
+
|
|
183
|
+
| Method | Description |
|
|
184
|
+
| -------------------------------------- | ------------------------------------ |
|
|
185
|
+
| `getRoles(user)` | Get user's Okta groups |
|
|
186
|
+
| `hasRole(user, role)` | Check if user has a specific group |
|
|
187
|
+
| `getPermissions(user)` | Get resolved permissions from groups |
|
|
188
|
+
| `hasPermission(user, permission)` | Check if user has a permission |
|
|
189
|
+
| `hasAllPermissions(user, permissions)` | Check if user has all permissions |
|
|
190
|
+
| `hasAnyPermission(user, permissions)` | Check if user has any permission |
|
|
191
|
+
|
|
192
|
+
### MastraAuthOkta
|
|
193
|
+
|
|
194
|
+
| Method | Description |
|
|
195
|
+
| ----------------------------------- | --------------------------- |
|
|
196
|
+
| `authenticateToken(token, request)` | Verify JWT and return user |
|
|
197
|
+
| `authorizeUser(user, request)` | Check if user is authorized |
|
|
198
|
+
|
|
199
|
+
## Requirements
|
|
200
|
+
|
|
201
|
+
- Node.js 22.13.0 or later
|
|
202
|
+
- Okta account with:
|
|
203
|
+
- OAuth application (for authentication)
|
|
204
|
+
- API token (for RBAC group fetching)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MastraAuthOkta - Okta authentication provider for Mastra with SSO support.
|
|
3
|
+
*
|
|
4
|
+
* Supports OAuth 2.0 / OIDC login flow with client_secret and session management.
|
|
5
|
+
*/
|
|
6
|
+
import type { ISSOProvider, ISessionProvider, IUserProvider, Session, SSOCallbackResult, SSOLoginConfig } from '@mastra/core/auth';
|
|
7
|
+
import { MastraAuthProvider } from '@mastra/core/server';
|
|
8
|
+
import type { HonoRequest } from 'hono';
|
|
9
|
+
import type { OktaUser, MastraAuthOktaOptions } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Mastra authentication provider for Okta with SSO support.
|
|
12
|
+
*
|
|
13
|
+
* Implements OAuth 2.0 / OIDC login flow with encrypted session cookies.
|
|
14
|
+
*
|
|
15
|
+
* @example Basic usage with SSO
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { MastraAuthOkta } from '@mastra/auth-okta';
|
|
18
|
+
*
|
|
19
|
+
* const auth = new MastraAuthOkta({
|
|
20
|
+
* domain: 'dev-123456.okta.com',
|
|
21
|
+
* clientId: 'your-client-id',
|
|
22
|
+
* clientSecret: 'your-client-secret',
|
|
23
|
+
* redirectUri: 'http://localhost:4111/api/auth/callback',
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class MastraAuthOkta extends MastraAuthProvider<OktaUser> implements ISSOProvider<OktaUser>, ISessionProvider<Session>, IUserProvider<OktaUser> {
|
|
28
|
+
protected domain: string;
|
|
29
|
+
protected clientId: string;
|
|
30
|
+
protected clientSecret: string;
|
|
31
|
+
protected issuer: string;
|
|
32
|
+
protected redirectUri: string;
|
|
33
|
+
protected scopes: string[];
|
|
34
|
+
protected cookieName: string;
|
|
35
|
+
protected cookieMaxAge: number;
|
|
36
|
+
protected cookiePassword: string;
|
|
37
|
+
protected secureCookies: boolean;
|
|
38
|
+
protected apiToken?: string;
|
|
39
|
+
private jwks;
|
|
40
|
+
constructor(options?: MastraAuthOktaOptions);
|
|
41
|
+
/**
|
|
42
|
+
* Authenticate a token from the request.
|
|
43
|
+
* First tries to read from session cookie, then falls back to Authorization header.
|
|
44
|
+
*/
|
|
45
|
+
authenticateToken(token: string, request: HonoRequest | Request): Promise<OktaUser | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Authorize a user.
|
|
48
|
+
*/
|
|
49
|
+
authorizeUser(user: OktaUser, _request: HonoRequest): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Get the current user from the request session.
|
|
52
|
+
*/
|
|
53
|
+
getCurrentUser(request: Request): Promise<OktaUser | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Get a user by ID via the Okta Users API.
|
|
56
|
+
* Requires an API token (set OKTA_API_TOKEN or pass apiToken in options).
|
|
57
|
+
* Returns null if no API token is configured or user is not found.
|
|
58
|
+
*/
|
|
59
|
+
getUser(userId: string): Promise<OktaUser | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Get user from session cookie.
|
|
62
|
+
*/
|
|
63
|
+
private getUserFromSession;
|
|
64
|
+
/**
|
|
65
|
+
* Extract the raw ID token from the encrypted session cookie.
|
|
66
|
+
* Used to provide id_token_hint for Okta logout.
|
|
67
|
+
*/
|
|
68
|
+
private getIdTokenFromSession;
|
|
69
|
+
/**
|
|
70
|
+
* Get the URL to redirect users to for Okta login.
|
|
71
|
+
* Uses client_secret authentication (no PKCE) since this is a confidential client.
|
|
72
|
+
*/
|
|
73
|
+
getLoginUrl(redirectUri: string, state: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Handle the OAuth callback from Okta.
|
|
76
|
+
* Note: The server passes only the stateId (UUID part), not the full state.
|
|
77
|
+
*/
|
|
78
|
+
handleCallback(code: string, stateId: string): Promise<SSOCallbackResult<OktaUser>>;
|
|
79
|
+
/**
|
|
80
|
+
* Get the URL to redirect users to for logout.
|
|
81
|
+
* Includes id_token_hint from session when available (required by Okta).
|
|
82
|
+
*/
|
|
83
|
+
getLogoutUrl(redirectUri: string, request?: Request): Promise<string | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Get cookies to set during login.
|
|
86
|
+
*/
|
|
87
|
+
getLoginCookies(_state: string): string[];
|
|
88
|
+
/**
|
|
89
|
+
* Get the configuration for rendering the login button.
|
|
90
|
+
*/
|
|
91
|
+
getLoginButtonConfig(): SSOLoginConfig;
|
|
92
|
+
createSession(userId: string, metadata?: Record<string, unknown>): Promise<Session>;
|
|
93
|
+
validateSession(_sessionId: string): Promise<Session | null>;
|
|
94
|
+
destroySession(_sessionId: string): Promise<void>;
|
|
95
|
+
refreshSession(_sessionId: string): Promise<Session | null>;
|
|
96
|
+
getSessionIdFromRequest(_request: Request): string | null;
|
|
97
|
+
getSessionHeaders(_session: Session): Record<string, string>;
|
|
98
|
+
getClearSessionHeaders(): Record<string, string>;
|
|
99
|
+
/**
|
|
100
|
+
* Build consistent cookie attribute string for set/clear operations.
|
|
101
|
+
*/
|
|
102
|
+
private cookieFlags;
|
|
103
|
+
/**
|
|
104
|
+
* Get the Okta domain.
|
|
105
|
+
*/
|
|
106
|
+
getDomain(): string;
|
|
107
|
+
/**
|
|
108
|
+
* Get the configured client ID.
|
|
109
|
+
*/
|
|
110
|
+
getClientId(): string;
|
|
111
|
+
/**
|
|
112
|
+
* Get the configured redirect URI.
|
|
113
|
+
*/
|
|
114
|
+
getRedirectUri(): string;
|
|
115
|
+
/**
|
|
116
|
+
* Get the issuer URL.
|
|
117
|
+
*/
|
|
118
|
+
getIssuer(): string;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=auth-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.d.ts","sourceRoot":"","sources":["../src/auth-provider.ts"],"names":[],"mappings":"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"}
|