0
# OAuth Service
1
2
Interface for OAuth service implementations providing token generation, refresh, and destruction operations for OAuth-based authentication flows.
3
4
## Capabilities
5
6
### Static Methods
7
8
Type checking and identification methods for OAuth services.
9
10
```javascript { .api }
11
/**
12
* Static property identifying this as an OAuth service
13
*/
14
static _isOauthService: boolean;
15
16
/**
17
* Checks if an object is an OAuth service
18
* @param {object} obj - Object to check
19
* @returns {boolean} True if obj is an OAuth service
20
*/
21
static isOauthService(obj: object): boolean;
22
```
23
24
### Core OAuth Operations
25
26
Abstract methods that must be implemented by child classes for OAuth token management.
27
28
```javascript { .api }
29
/**
30
* Generates an OAuth token for authentication
31
* @returns {any} Generated token result
32
* @throws {Error} If not overridden by child class
33
*/
34
generateToken(): any;
35
36
/**
37
* Refreshes an existing OAuth token
38
* @returns {any} Refreshed token result
39
* @throws {Error} If not overridden by child class
40
*/
41
refreshToken(): any;
42
43
/**
44
* Destroys/revokes an OAuth token
45
* @returns {any} Token destruction result
46
* @throws {Error} If not overridden by child class
47
*/
48
destroyToken(): any;
49
```
50
51
## Implementation Example
52
53
```javascript
54
import { OauthService } from "medusa-interfaces";
55
import jwt from "jsonwebtoken";
56
import crypto from "crypto";
57
58
class JWTOauthService extends OauthService {
59
constructor(options) {
60
super();
61
this.secretKey = options.secret_key;
62
this.refreshTokenSecret = options.refresh_token_secret;
63
this.accessTokenExpiry = options.access_token_expiry || "15m";
64
this.refreshTokenExpiry = options.refresh_token_expiry || "7d";
65
this.tokenStorage = options.token_storage || {}; // In-memory storage for demo
66
}
67
68
async generateToken(payload) {
69
// Generate access token
70
const accessToken = jwt.sign(
71
{
72
...payload,
73
type: "access",
74
jti: crypto.randomUUID() // Unique token ID
75
},
76
this.secretKey,
77
{ expiresIn: this.accessTokenExpiry }
78
);
79
80
// Generate refresh token
81
const refreshTokenId = crypto.randomUUID();
82
const refreshToken = jwt.sign(
83
{
84
jti: refreshTokenId,
85
type: "refresh",
86
user_id: payload.user_id
87
},
88
this.refreshTokenSecret,
89
{ expiresIn: this.refreshTokenExpiry }
90
);
91
92
// Store refresh token (in production, use Redis/database)
93
this.tokenStorage[refreshTokenId] = {
94
user_id: payload.user_id,
95
created_at: new Date(),
96
is_active: true
97
};
98
99
return {
100
access_token: accessToken,
101
refresh_token: refreshToken,
102
token_type: "Bearer",
103
expires_in: this.parseExpiry(this.accessTokenExpiry),
104
scope: payload.scope || "read write"
105
};
106
}
107
108
async refreshToken(refreshTokenString) {
109
try {
110
// Verify refresh token
111
const decoded = jwt.verify(refreshTokenString, this.refreshTokenSecret);
112
113
if (decoded.type !== "refresh") {
114
throw new Error("Invalid token type");
115
}
116
117
// Check if refresh token is still active
118
const storedToken = this.tokenStorage[decoded.jti];
119
if (!storedToken || !storedToken.is_active) {
120
throw new Error("Refresh token revoked or invalid");
121
}
122
123
// Generate new access token
124
const newAccessToken = jwt.sign(
125
{
126
user_id: storedToken.user_id,
127
type: "access",
128
jti: crypto.randomUUID()
129
},
130
this.secretKey,
131
{ expiresIn: this.accessTokenExpiry }
132
);
133
134
return {
135
access_token: newAccessToken,
136
token_type: "Bearer",
137
expires_in: this.parseExpiry(this.accessTokenExpiry)
138
};
139
140
} catch (error) {
141
throw new Error(`Token refresh failed: ${error.message}`);
142
}
143
}
144
145
async destroyToken(tokenString, tokenType = "access") {
146
try {
147
let decoded;
148
149
if (tokenType === "access") {
150
decoded = jwt.verify(tokenString, this.secretKey);
151
} else if (tokenType === "refresh") {
152
decoded = jwt.verify(tokenString, this.refreshTokenSecret);
153
154
// Mark refresh token as inactive
155
if (this.tokenStorage[decoded.jti]) {
156
this.tokenStorage[decoded.jti].is_active = false;
157
}
158
}
159
160
// In production, you might maintain a blacklist of revoked tokens
161
// until their natural expiration
162
163
return {
164
success: true,
165
token_id: decoded.jti,
166
revoked_at: new Date()
167
};
168
169
} catch (error) {
170
throw new Error(`Token destruction failed: ${error.message}`);
171
}
172
}
173
174
parseExpiry(expiry) {
175
// Convert JWT expiry format to seconds
176
const unit = expiry.slice(-1);
177
const value = parseInt(expiry.slice(0, -1));
178
179
switch (unit) {
180
case "s": return value;
181
case "m": return value * 60;
182
case "h": return value * 3600;
183
case "d": return value * 86400;
184
default: return 900; // 15 minutes default
185
}
186
}
187
}
188
189
// OAuth2 Provider Implementation
190
class OAuth2Service extends OauthService {
191
constructor(options) {
192
super();
193
this.clientId = options.client_id;
194
this.clientSecret = options.client_secret;
195
this.providerUrl = options.provider_url;
196
this.redirectUri = options.redirect_uri;
197
}
198
199
async generateToken(authorizationCode) {
200
// Exchange authorization code for tokens
201
const tokenRequest = {
202
grant_type: "authorization_code",
203
code: authorizationCode,
204
client_id: this.clientId,
205
client_secret: this.clientSecret,
206
redirect_uri: this.redirectUri
207
};
208
209
try {
210
const response = await fetch(`${this.providerUrl}/oauth/token`, {
211
method: "POST",
212
headers: {
213
"Content-Type": "application/x-www-form-urlencoded"
214
},
215
body: new URLSearchParams(tokenRequest)
216
});
217
218
if (!response.ok) {
219
throw new Error(`Token exchange failed: ${response.statusText}`);
220
}
221
222
const tokenData = await response.json();
223
224
return {
225
access_token: tokenData.access_token,
226
refresh_token: tokenData.refresh_token,
227
token_type: tokenData.token_type || "Bearer",
228
expires_in: tokenData.expires_in,
229
scope: tokenData.scope
230
};
231
232
} catch (error) {
233
throw new Error(`OAuth token generation failed: ${error.message}`);
234
}
235
}
236
237
async refreshToken(refreshTokenString) {
238
const refreshRequest = {
239
grant_type: "refresh_token",
240
refresh_token: refreshTokenString,
241
client_id: this.clientId,
242
client_secret: this.clientSecret
243
};
244
245
try {
246
const response = await fetch(`${this.providerUrl}/oauth/token`, {
247
method: "POST",
248
headers: {
249
"Content-Type": "application/x-www-form-urlencoded"
250
},
251
body: new URLSearchParams(refreshRequest)
252
});
253
254
if (!response.ok) {
255
throw new Error(`Token refresh failed: ${response.statusText}`);
256
}
257
258
const tokenData = await response.json();
259
260
return {
261
access_token: tokenData.access_token,
262
refresh_token: tokenData.refresh_token || refreshTokenString,
263
token_type: tokenData.token_type || "Bearer",
264
expires_in: tokenData.expires_in,
265
scope: tokenData.scope
266
};
267
268
} catch (error) {
269
throw new Error(`OAuth token refresh failed: ${error.message}`);
270
}
271
}
272
273
async destroyToken(tokenString, tokenType = "access") {
274
const revokeRequest = {
275
token: tokenString,
276
token_type_hint: tokenType,
277
client_id: this.clientId,
278
client_secret: this.clientSecret
279
};
280
281
try {
282
const response = await fetch(`${this.providerUrl}/oauth/revoke`, {
283
method: "POST",
284
headers: {
285
"Content-Type": "application/x-www-form-urlencoded"
286
},
287
body: new URLSearchParams(revokeRequest)
288
});
289
290
// OAuth2 revoke endpoint typically returns 200 even for invalid tokens
291
return {
292
success: response.ok,
293
revoked_at: new Date()
294
};
295
296
} catch (error) {
297
throw new Error(`OAuth token revocation failed: ${error.message}`);
298
}
299
}
300
301
// Helper method to generate authorization URL
302
getAuthorizationUrl(scopes = [], state = null) {
303
const params = new URLSearchParams({
304
response_type: "code",
305
client_id: this.clientId,
306
redirect_uri: this.redirectUri,
307
scope: scopes.join(" ")
308
});
309
310
if (state) {
311
params.set("state", state);
312
}
313
314
return `${this.providerUrl}/oauth/authorize?${params.toString()}`;
315
}
316
}
317
```
318
319
## Usage in Medusa
320
321
OAuth services are typically used for:
322
323
- **Admin Authentication**: JWT tokens for admin panel access
324
- **Customer Authentication**: Customer login sessions
325
- **API Access**: Third-party integrations with OAuth providers
326
- **Service-to-Service**: Inter-service authentication
327
328
**Basic Usage Pattern:**
329
330
```javascript
331
// In a Medusa authentication service
332
class AuthService {
333
constructor({ oauthService }) {
334
this.oauthService_ = oauthService;
335
}
336
337
async login(credentials) {
338
// Validate credentials
339
const user = await this.validateCredentials(credentials);
340
341
// Generate OAuth tokens
342
const tokens = await this.oauthService_.generateToken({
343
user_id: user.id,
344
email: user.email,
345
scope: user.role
346
});
347
348
return {
349
user: user,
350
...tokens
351
};
352
}
353
354
async refreshUserToken(refreshToken) {
355
try {
356
const newTokens = await this.oauthService_.refreshToken(refreshToken);
357
return newTokens;
358
} catch (error) {
359
throw new Error("Token refresh failed - please login again");
360
}
361
}
362
363
async logout(accessToken) {
364
await this.oauthService_.destroyToken(accessToken, "access");
365
return { success: true };
366
}
367
}
368
```
369
370
## Token Validation Middleware
371
372
```javascript
373
// Express middleware for token validation
374
function createAuthMiddleware(oauthService) {
375
return async (req, res, next) => {
376
const authHeader = req.headers.authorization;
377
378
if (!authHeader || !authHeader.startsWith("Bearer ")) {
379
return res.status(401).json({ error: "Missing or invalid authorization header" });
380
}
381
382
const token = authHeader.substring(7);
383
384
try {
385
// In a real implementation, you'd verify the token
386
const decoded = jwt.verify(token, process.env.JWT_SECRET);
387
req.user = decoded;
388
next();
389
} catch (error) {
390
res.status(401).json({ error: "Invalid or expired token" });
391
}
392
};
393
}
394
```
395
396
## Error Handling
397
398
All abstract methods throw descriptive errors when not implemented:
399
400
- `"generateToken must be overridden by the child class"`
401
- `"refreshToken must be overridden by the child class"`
402
- `"destroyToken must be overridden by the child class"`
403
404
## Security Considerations
405
406
When implementing OAuth services:
407
408
- **Use strong secrets** for token signing
409
- **Implement token rotation** for refresh tokens
410
- **Set appropriate expiry times** for different token types
411
- **Maintain token blacklists** for revoked tokens
412
- **Use HTTPS** for all token exchanges
413
- **Validate token audience and issuer** claims
414
- **Implement rate limiting** for token endpoints