backend-manager 5.0.42 → 5.0.44
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/package.json +1 -1
- package/src/manager/routes/user/delete.js +8 -7
- package/src/manager/routes/user/oauth2/_helpers.js +176 -0
- package/src/manager/routes/user/oauth2/delete.js +42 -0
- package/src/manager/routes/user/oauth2/get.js +132 -0
- package/src/manager/routes/user/oauth2/post.js +139 -239
- package/src/manager/routes/user/oauth2/providers/discord.js +26 -3
- package/src/manager/routes/user/oauth2/providers/google.js +31 -3
- package/src/manager/routes/user/sessions/delete.js +5 -4
- package/src/manager/schemas/user/oauth2/delete.js +11 -0
- package/src/manager/schemas/user/oauth2/get.js +27 -0
- package/src/manager/schemas/user/oauth2/post.js +7 -59
- package/templates/_.gitignore +1 -1
package/package.json
CHANGED
|
@@ -49,10 +49,10 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
|
49
49
|
response: 'json',
|
|
50
50
|
tries: 2,
|
|
51
51
|
log: true,
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
body: {
|
|
53
|
+
uid,
|
|
54
|
+
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
54
55
|
},
|
|
55
|
-
body: { uid },
|
|
56
56
|
})
|
|
57
57
|
.then((json) => {
|
|
58
58
|
assistant.log(`Sign out of all sessions success`, json);
|
|
@@ -62,10 +62,11 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
// Delete the user
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
});
|
|
65
|
+
try {
|
|
66
|
+
await admin.auth().deleteUser(uid);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return assistant.respond(`Failed to delete user: ${e}`, { code: 500 });
|
|
69
|
+
}
|
|
69
70
|
|
|
70
71
|
return assistant.respond({ success: true });
|
|
71
72
|
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fetch = require('wonderful-fetch');
|
|
3
|
+
const { arrayify } = require('node-powertools');
|
|
4
|
+
|
|
5
|
+
// Constants
|
|
6
|
+
const STATE_TTL_MINUTES = 10;
|
|
7
|
+
|
|
8
|
+
// Derive OAuth state encryption key from BACKEND_MANAGER_KEY
|
|
9
|
+
const STATE_KEY = process.env.BACKEND_MANAGER_KEY
|
|
10
|
+
? crypto.createHash('sha256').update(`oauth2-state:${process.env.BACKEND_MANAGER_KEY}`).digest('hex')
|
|
11
|
+
: null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build context object with common OAuth2 data
|
|
15
|
+
* Used by GET, POST, DELETE handlers
|
|
16
|
+
*/
|
|
17
|
+
async function buildContext({ assistant, Manager, user, settings, libraries, requireProvider = true }) {
|
|
18
|
+
const { admin } = libraries;
|
|
19
|
+
|
|
20
|
+
// Require authentication
|
|
21
|
+
if (!user.authenticated) {
|
|
22
|
+
return { error: { message: 'Authentication required', code: 401 } };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get target user (admin can manage other users)
|
|
26
|
+
const targetUid = settings.uid || user.auth.uid;
|
|
27
|
+
|
|
28
|
+
if (targetUid !== user.auth.uid && !user.roles.admin) {
|
|
29
|
+
return { error: { message: 'Admin required to manage other users', code: 403 } };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Resolve target user data
|
|
33
|
+
let targetUser = user;
|
|
34
|
+
|
|
35
|
+
if (targetUid !== user.auth.uid) {
|
|
36
|
+
const doc = await admin.firestore().doc(`users/${targetUid}`).get();
|
|
37
|
+
|
|
38
|
+
if (!doc.exists) {
|
|
39
|
+
return { error: { message: 'User not found', code: 404 } };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
targetUser = doc.data();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build redirect URI
|
|
46
|
+
const redirectUri = assistant.isDevelopment()
|
|
47
|
+
? 'https://localhost:4000/oauth2'
|
|
48
|
+
: `${Manager.config.brand.url}/oauth2`;
|
|
49
|
+
|
|
50
|
+
// If provider not required (e.g., tokenize gets it from encrypted state), skip loading
|
|
51
|
+
if (!requireProvider) {
|
|
52
|
+
return {
|
|
53
|
+
assistant,
|
|
54
|
+
Manager,
|
|
55
|
+
admin,
|
|
56
|
+
settings,
|
|
57
|
+
targetUid,
|
|
58
|
+
targetUser,
|
|
59
|
+
redirectUri,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Provider is required
|
|
64
|
+
if (!settings.provider) {
|
|
65
|
+
return { error: { message: 'The provider parameter is required', code: 400 } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Load provider module
|
|
69
|
+
let oauth2Provider;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
oauth2Provider = require(`./providers/${settings.provider}.js`);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return { error: { message: `Unknown OAuth2 provider: ${settings.provider}`, code: 400 } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get OAuth2 credentials
|
|
78
|
+
const providerEnvKey = settings.provider.toUpperCase().replace(/-/g, '_');
|
|
79
|
+
const clientId = process.env[`OAUTH2_${providerEnvKey}_CLIENT_ID`];
|
|
80
|
+
const clientSecret = process.env[`OAUTH2_${providerEnvKey}_CLIENT_SECRET`];
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
assistant,
|
|
84
|
+
Manager,
|
|
85
|
+
admin,
|
|
86
|
+
oauth2Provider,
|
|
87
|
+
settings,
|
|
88
|
+
targetUid,
|
|
89
|
+
targetUser,
|
|
90
|
+
clientId,
|
|
91
|
+
clientSecret,
|
|
92
|
+
redirectUri,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Load provider and credentials from provider name
|
|
98
|
+
*/
|
|
99
|
+
function loadProvider(providerName) {
|
|
100
|
+
let oauth2Provider;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
oauth2Provider = require(`./providers/${providerName}.js`);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return { error: { message: `Unknown OAuth2 provider: ${providerName}`, code: 400 } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const providerEnvKey = providerName.toUpperCase().replace(/-/g, '_');
|
|
109
|
+
const clientId = process.env[`OAUTH2_${providerEnvKey}_CLIENT_ID`];
|
|
110
|
+
const clientSecret = process.env[`OAUTH2_${providerEnvKey}_CLIENT_SECRET`];
|
|
111
|
+
|
|
112
|
+
return { oauth2Provider, clientId, clientSecret };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Crypto Helpers
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
function generateCsrfToken() {
|
|
120
|
+
return crypto.randomBytes(32).toString('hex');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function encryptState(data) {
|
|
124
|
+
if (!STATE_KEY) {
|
|
125
|
+
throw new Error('BACKEND_MANAGER_KEY not configured');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const iv = crypto.randomBytes(16);
|
|
129
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(STATE_KEY, 'hex'), iv);
|
|
130
|
+
|
|
131
|
+
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'base64');
|
|
132
|
+
encrypted += cipher.final('base64');
|
|
133
|
+
|
|
134
|
+
const authTag = cipher.getAuthTag().toString('base64');
|
|
135
|
+
|
|
136
|
+
return `${iv.toString('base64')}.${encrypted}.${authTag}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function decryptState(encryptedState) {
|
|
140
|
+
if (!STATE_KEY) {
|
|
141
|
+
throw new Error('BACKEND_MANAGER_KEY not configured');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const parts = encryptedState.split('.');
|
|
145
|
+
|
|
146
|
+
if (parts.length !== 3) {
|
|
147
|
+
throw new Error('Invalid state format');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const [ivB64, encryptedB64, authTagB64] = parts;
|
|
151
|
+
|
|
152
|
+
const iv = Buffer.from(ivB64, 'base64');
|
|
153
|
+
const encrypted = Buffer.from(encryptedB64, 'base64');
|
|
154
|
+
const authTag = Buffer.from(authTagB64, 'base64');
|
|
155
|
+
|
|
156
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(STATE_KEY, 'hex'), iv);
|
|
157
|
+
decipher.setAuthTag(authTag);
|
|
158
|
+
|
|
159
|
+
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
|
|
160
|
+
decrypted += decipher.final('utf8');
|
|
161
|
+
|
|
162
|
+
return JSON.parse(decrypted);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
STATE_TTL_MINUTES,
|
|
167
|
+
STATE_KEY,
|
|
168
|
+
buildContext,
|
|
169
|
+
loadProvider,
|
|
170
|
+
generateCsrfToken,
|
|
171
|
+
encryptState,
|
|
172
|
+
decryptState,
|
|
173
|
+
// Re-export utilities for handlers
|
|
174
|
+
fetch,
|
|
175
|
+
arrayify,
|
|
176
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const {
|
|
2
|
+
buildContext,
|
|
3
|
+
} = require('./_helpers.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DELETE /user/oauth2 - Remove OAuth connection
|
|
7
|
+
*
|
|
8
|
+
* Revokes tokens with the provider (best effort) and removes the connection.
|
|
9
|
+
*/
|
|
10
|
+
module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
11
|
+
const context = await buildContext({ assistant, Manager, user, settings, libraries });
|
|
12
|
+
|
|
13
|
+
if (context.error) {
|
|
14
|
+
return assistant.respond(context.error.message, { code: context.error.code });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { admin, oauth2Provider, targetUid, targetUser, clientId, clientSecret } = context;
|
|
18
|
+
|
|
19
|
+
assistant.log('OAuth2 DELETE request', { provider: settings.provider });
|
|
20
|
+
|
|
21
|
+
// Get current access token to revoke
|
|
22
|
+
const accessToken = targetUser?.oauth2?.[settings.provider]?.token?.access_token;
|
|
23
|
+
|
|
24
|
+
// Attempt to revoke token with provider (best effort)
|
|
25
|
+
if (accessToken && oauth2Provider.revokeToken) {
|
|
26
|
+
const revokeResult = await oauth2Provider.revokeToken(accessToken, {
|
|
27
|
+
assistant,
|
|
28
|
+
clientId,
|
|
29
|
+
clientSecret,
|
|
30
|
+
}).catch(e => ({ revoked: false, reason: e.message }));
|
|
31
|
+
|
|
32
|
+
assistant.log('Token revocation result:', revokeResult);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Delete OAuth data from user document
|
|
36
|
+
await admin.firestore().doc(`users/${targetUid}`).update({
|
|
37
|
+
[`oauth2.${settings.provider}`]: admin.firestore.FieldValue.delete(),
|
|
38
|
+
metadata: Manager.Metadata().set({ tag: 'user/oauth2' }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return assistant.respond({ success: true });
|
|
42
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const {
|
|
2
|
+
buildContext,
|
|
3
|
+
generateCsrfToken,
|
|
4
|
+
encryptState,
|
|
5
|
+
} = require('./_helpers.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /user/oauth2 - Read operations
|
|
9
|
+
*
|
|
10
|
+
* Actions:
|
|
11
|
+
* - authorize (default): Get authorization URL
|
|
12
|
+
* - status: Check connection status
|
|
13
|
+
*/
|
|
14
|
+
module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
15
|
+
const context = await buildContext({ assistant, Manager, user, settings, libraries });
|
|
16
|
+
|
|
17
|
+
if (context.error) {
|
|
18
|
+
return assistant.respond(context.error.message, { code: context.error.code });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { admin, oauth2Provider, targetUid, targetUser, clientId, clientSecret, redirectUri } = context;
|
|
22
|
+
|
|
23
|
+
assistant.log('OAuth2 GET request', { action: settings.action, provider: settings.provider });
|
|
24
|
+
|
|
25
|
+
switch (settings.action) {
|
|
26
|
+
case 'status':
|
|
27
|
+
return processStatus(context);
|
|
28
|
+
|
|
29
|
+
case 'authorize':
|
|
30
|
+
default:
|
|
31
|
+
return processAuthorize(context);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Handlers
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
async function processAuthorize(context) {
|
|
40
|
+
const { assistant, Manager, admin, oauth2Provider, settings, targetUid, clientId, redirectUri } = context;
|
|
41
|
+
|
|
42
|
+
if (!clientId) {
|
|
43
|
+
return assistant.respond(`Missing client_id for ${settings.provider} provider`, { code: 500 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Generate CSRF token
|
|
47
|
+
const csrfToken = generateCsrfToken();
|
|
48
|
+
|
|
49
|
+
// Store CSRF token in user's usage document (auto-cleaned daily)
|
|
50
|
+
await admin.firestore().doc(`usage/${targetUid}`).set({
|
|
51
|
+
oauth2: {
|
|
52
|
+
[settings.provider]: {
|
|
53
|
+
csrf: csrfToken,
|
|
54
|
+
createdAt: Date.now(),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}, { merge: true });
|
|
58
|
+
|
|
59
|
+
// Build minimal state (no unnecessary data)
|
|
60
|
+
const stateData = {
|
|
61
|
+
provider: settings.provider,
|
|
62
|
+
uid: targetUid,
|
|
63
|
+
csrf: csrfToken,
|
|
64
|
+
ts: Date.now(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Encrypt state
|
|
68
|
+
let encryptedState;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
encryptedState = encryptState(stateData);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return assistant.respond(e.message, { code: 500 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build authorization URL
|
|
77
|
+
const url = new URL(oauth2Provider.urls.authorize);
|
|
78
|
+
url.searchParams.set('state', encryptedState);
|
|
79
|
+
url.searchParams.set('client_id', clientId);
|
|
80
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
81
|
+
url.searchParams.set('response_type', 'code');
|
|
82
|
+
|
|
83
|
+
// Set scopes from app config, fall back to provider defaults
|
|
84
|
+
const appScopes = Manager.config?.oauth2?.[settings.provider]?.scope || [];
|
|
85
|
+
const finalScopes = appScopes.length > 0 ? appScopes : (oauth2Provider.scope || []);
|
|
86
|
+
url.searchParams.set('scope', finalScopes.join(' '));
|
|
87
|
+
|
|
88
|
+
// Add provider-specific auth params
|
|
89
|
+
const authParams = oauth2Provider.authParams || {};
|
|
90
|
+
|
|
91
|
+
for (const [key, value] of Object.entries(authParams)) {
|
|
92
|
+
url.searchParams.set(key, value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const urlString = url.toString();
|
|
96
|
+
|
|
97
|
+
assistant.log('OAuth2 authorize URL generated');
|
|
98
|
+
|
|
99
|
+
if (settings.redirect) {
|
|
100
|
+
return assistant.redirect(urlString);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return assistant.respond({ url: urlString });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function processStatus(context) {
|
|
107
|
+
const { assistant, Manager, admin, oauth2Provider, settings, targetUid, targetUser, clientId, clientSecret } = context;
|
|
108
|
+
|
|
109
|
+
const token = targetUser?.oauth2?.[settings.provider]?.token?.refresh_token;
|
|
110
|
+
|
|
111
|
+
if (!token) {
|
|
112
|
+
return assistant.respond({ status: 'disconnected' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Verify connection if provider supports it
|
|
116
|
+
if (oauth2Provider.verifyConnection) {
|
|
117
|
+
const status = await oauth2Provider.verifyConnection(token, { Manager, assistant, clientId, clientSecret })
|
|
118
|
+
.catch(() => 'error');
|
|
119
|
+
|
|
120
|
+
if ((status === 'disconnected' || status === 'error') && settings.removeInvalidTokens) {
|
|
121
|
+
await admin.firestore().doc(`users/${targetUid}`).update({
|
|
122
|
+
[`oauth2.${settings.provider}`]: admin.firestore.FieldValue.delete(),
|
|
123
|
+
metadata: Manager.Metadata().set({ tag: 'user/oauth2' }),
|
|
124
|
+
});
|
|
125
|
+
assistant.log(`Removed invalid token for user: ${targetUid}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return assistant.respond({ status });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return assistant.respond({ status: 'connected' });
|
|
132
|
+
}
|
|
@@ -1,326 +1,226 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const {
|
|
2
|
+
buildContext,
|
|
3
|
+
loadProvider,
|
|
4
|
+
decryptState,
|
|
5
|
+
STATE_TTL_MINUTES,
|
|
6
|
+
fetch,
|
|
7
|
+
} = require('./_helpers.js');
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
* POST /user/oauth2 -
|
|
10
|
+
* POST /user/oauth2 - Write operations
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* - tokenize: Exchange code for tokens
|
|
12
|
+
* Actions:
|
|
13
|
+
* - tokenize (default): Exchange authorization code for tokens
|
|
11
14
|
* - refresh: Refresh access token
|
|
12
|
-
* - deauthorize: Remove OAuth2 connection
|
|
13
|
-
* - status: Check connection status
|
|
14
15
|
*/
|
|
15
16
|
module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
16
17
|
const { admin } = libraries;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
if (!user.authenticated) {
|
|
20
|
-
return assistant.respond('Authentication required', { code: 401 });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Require admin to manage other users' OAuth
|
|
24
|
-
const uid = settings.uid;
|
|
25
|
-
|
|
26
|
-
if (uid !== user.auth.uid && !user.roles.admin) {
|
|
27
|
-
return assistant.respond('Admin required', { code: 403 });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Get target user data
|
|
31
|
-
let userData = user;
|
|
19
|
+
assistant.log('OAuth2 POST request', { action: settings.action });
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!doc.exists) {
|
|
37
|
-
return assistant.respond('User not found', { code: 404 });
|
|
38
|
-
}
|
|
21
|
+
switch (settings.action) {
|
|
22
|
+
case 'refresh':
|
|
23
|
+
return processRefresh({ assistant, Manager, user, settings, libraries });
|
|
39
24
|
|
|
40
|
-
|
|
25
|
+
case 'tokenize':
|
|
26
|
+
default:
|
|
27
|
+
return processTokenize({ assistant, Manager, admin, settings });
|
|
41
28
|
}
|
|
29
|
+
};
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Handlers
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
async function processTokenize({ assistant, Manager, admin, settings }) {
|
|
36
|
+
assistant.log('processTokenize settings', {
|
|
37
|
+
hasCode: !!settings.code,
|
|
38
|
+
codeType: typeof settings.code,
|
|
39
|
+
codeLength: settings.code?.length,
|
|
40
|
+
hasEncryptedState: !!settings.encryptedState,
|
|
41
|
+
encryptedStateLength: settings.encryptedState?.length,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Validate required params
|
|
45
|
+
if (!settings.code) {
|
|
46
|
+
return assistant.respond('Missing authorization code', { code: 400 });
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
oauth2 = require(`./providers/${settings.provider}.js`);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
return assistant.respond(`Unknown OAuth2 provider: ${settings.provider}`, { code: 400 });
|
|
49
|
+
if (!settings.encryptedState) {
|
|
50
|
+
return assistant.respond('Missing encrypted state', { code: 400 });
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
// Build
|
|
58
|
-
const
|
|
53
|
+
// Build redirect URI
|
|
54
|
+
const redirectUri = assistant.isDevelopment()
|
|
59
55
|
? 'https://localhost:4000/oauth2'
|
|
60
56
|
: `${Manager.config.brand.url}/oauth2`;
|
|
61
57
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const providerEnvKey = settings.provider.toUpperCase().replace(/-/g, '_');
|
|
65
|
-
const client_id = process.env[`OAUTH2_${providerEnvKey}_CLIENT_ID`];
|
|
66
|
-
const client_secret = process.env[`OAUTH2_${providerEnvKey}_CLIENT_SECRET`];
|
|
67
|
-
|
|
68
|
-
const state = settings.state;
|
|
69
|
-
|
|
70
|
-
assistant.log('OAuth2 settings', settings);
|
|
71
|
-
|
|
72
|
-
// Process by state
|
|
73
|
-
switch (state) {
|
|
74
|
-
case 'authorize':
|
|
75
|
-
return processAuthorize(assistant, Manager, settings, oauth2, ultimateJekyllOAuth2Url, client_id);
|
|
76
|
-
|
|
77
|
-
case 'tokenize':
|
|
78
|
-
return processTokenize(assistant, Manager, admin, settings, oauth2, ultimateJekyllOAuth2Url, client_id, client_secret, uid);
|
|
58
|
+
// Decrypt and validate state
|
|
59
|
+
let stateData;
|
|
79
60
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
try {
|
|
62
|
+
stateData = decryptState(settings.encryptedState);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
assistant.log('Failed to decrypt state:', e.message);
|
|
65
|
+
return assistant.respond('Invalid OAuth state', { code: 400 });
|
|
66
|
+
}
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
68
|
+
// Validate timestamp (10 min TTL)
|
|
69
|
+
const ageMinutes = (Date.now() - stateData.ts) / 1000 / 60;
|
|
88
70
|
|
|
89
|
-
|
|
90
|
-
|
|
71
|
+
if (ageMinutes > STATE_TTL_MINUTES) {
|
|
72
|
+
return assistant.respond('OAuth session expired. Please try again.', { code: 400 });
|
|
91
73
|
}
|
|
92
|
-
};
|
|
93
74
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
75
|
+
// Load provider from decrypted state
|
|
76
|
+
const providerResult = loadProvider(stateData.provider);
|
|
77
|
+
|
|
78
|
+
if (providerResult.error) {
|
|
79
|
+
return assistant.respond(providerResult.error.message, { code: providerResult.error.code });
|
|
97
80
|
}
|
|
98
81
|
|
|
99
|
-
|
|
100
|
-
const defaultReferrer = assistant.isDevelopment() ? 'https://localhost:4000/account' : `${Manager.config.brand.url}/account`;
|
|
101
|
-
const stateData = {
|
|
102
|
-
code: 'success',
|
|
103
|
-
provider: settings.provider,
|
|
104
|
-
authenticationToken: settings.authenticationToken,
|
|
105
|
-
serverUrl: settings.serverUrl || `${Manager.project.apiUrl}/backend-manager`,
|
|
106
|
-
referrer: settings.referrer || defaultReferrer,
|
|
107
|
-
redirectUrl: settings.redirect_uri || settings.referrer || defaultReferrer,
|
|
108
|
-
};
|
|
82
|
+
const { oauth2Provider, clientId, clientSecret } = providerResult;
|
|
109
83
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
url.searchParams.set('scope', arrayify(settings.scope).join(' '));
|
|
114
|
-
url.searchParams.set('redirect_uri', ultimateJekyllOAuth2Url);
|
|
115
|
-
url.searchParams.set('access_type', settings.access_type);
|
|
116
|
-
url.searchParams.set('prompt', settings.prompt);
|
|
117
|
-
url.searchParams.set('include_granted_scopes', settings.include_granted_scopes);
|
|
118
|
-
url.searchParams.set('response_type', settings.response_type);
|
|
84
|
+
// Retrieve stored CSRF token from user's usage document
|
|
85
|
+
const usageDocRef = admin.firestore().doc(`usage/${stateData.uid}`);
|
|
86
|
+
const usageDoc = await usageDocRef.get();
|
|
119
87
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
88
|
+
if (!usageDoc.exists) {
|
|
89
|
+
return assistant.respond('OAuth session not found. Please try again.', { code: 400 });
|
|
90
|
+
}
|
|
123
91
|
|
|
124
|
-
|
|
92
|
+
const storedCsrf = usageDoc.data()?.oauth2?.[stateData.provider]?.csrf;
|
|
125
93
|
|
|
126
|
-
if (
|
|
127
|
-
return assistant.
|
|
94
|
+
if (!storedCsrf) {
|
|
95
|
+
return assistant.respond('OAuth session not found. Please try again.', { code: 400 });
|
|
128
96
|
}
|
|
129
97
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
98
|
+
// Validate CSRF token
|
|
99
|
+
if (storedCsrf !== stateData.csrf) {
|
|
100
|
+
assistant.log('CSRF mismatch', { stored: storedCsrf, received: stateData.csrf });
|
|
101
|
+
return assistant.respond('Invalid OAuth session', { code: 400 });
|
|
102
|
+
}
|
|
135
103
|
|
|
104
|
+
// Exchange code for tokens
|
|
136
105
|
const body = {
|
|
137
|
-
client_id,
|
|
138
|
-
client_secret,
|
|
106
|
+
client_id: clientId,
|
|
107
|
+
client_secret: clientSecret,
|
|
139
108
|
grant_type: 'authorization_code',
|
|
140
|
-
redirect_uri:
|
|
109
|
+
redirect_uri: redirectUri,
|
|
141
110
|
code: settings.code,
|
|
142
111
|
};
|
|
143
112
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const tokenizeResponse = await fetch(oauth2.urls.tokenize, {
|
|
113
|
+
const tokenResponse = await fetch(oauth2Provider.urls.tokenize, {
|
|
147
114
|
method: 'POST',
|
|
148
115
|
timeout: 60000,
|
|
149
116
|
response: 'json',
|
|
150
|
-
tries: 1,
|
|
151
|
-
log: true,
|
|
152
117
|
body: new URLSearchParams(body),
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
156
|
-
},
|
|
157
|
-
}).catch((e) => e);
|
|
118
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
119
|
+
}).catch(e => e);
|
|
158
120
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (tokenizeResponse instanceof Error) {
|
|
162
|
-
return assistant.respond(tokenizeResponse.message, { code: 500 });
|
|
121
|
+
if (tokenResponse instanceof Error) {
|
|
122
|
+
return assistant.respond(`Token exchange failed: ${tokenResponse.message}`, { code: 500 });
|
|
163
123
|
}
|
|
164
124
|
|
|
165
|
-
// Verify identity
|
|
166
|
-
const verifiedIdentity = await
|
|
167
|
-
.catch(
|
|
168
|
-
|
|
169
|
-
assistant.log('verifiedIdentity', verifiedIdentity);
|
|
125
|
+
// Verify identity with provider
|
|
126
|
+
const verifiedIdentity = await oauth2Provider.verifyIdentity(tokenResponse, Manager, assistant)
|
|
127
|
+
.catch(e => e);
|
|
170
128
|
|
|
171
129
|
if (verifiedIdentity instanceof Error) {
|
|
172
130
|
return assistant.respond(verifiedIdentity.message, { code: 400 });
|
|
173
131
|
}
|
|
174
132
|
|
|
175
|
-
if (
|
|
133
|
+
if (!tokenResponse.refresh_token) {
|
|
176
134
|
return assistant.respond(
|
|
177
|
-
`Missing
|
|
135
|
+
`Missing refresh_token. Visit ${oauth2Provider.urls.removeAccess} and remove our app, then try again.`,
|
|
178
136
|
{ code: 400 }
|
|
179
137
|
);
|
|
180
138
|
}
|
|
181
139
|
|
|
182
|
-
// Store tokens
|
|
183
|
-
await admin.firestore().doc(`users/${uid}`)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
140
|
+
// Store tokens (only necessary fields, no raw settings)
|
|
141
|
+
await admin.firestore().doc(`users/${stateData.uid}`).set({
|
|
142
|
+
oauth2: {
|
|
143
|
+
[stateData.provider]: {
|
|
144
|
+
token: {
|
|
145
|
+
access_token: tokenResponse.access_token,
|
|
146
|
+
refresh_token: tokenResponse.refresh_token,
|
|
147
|
+
token_type: tokenResponse.token_type,
|
|
148
|
+
expires_in: tokenResponse.expires_in,
|
|
149
|
+
scope: tokenResponse.scope,
|
|
150
|
+
},
|
|
151
|
+
identity: verifiedIdentity,
|
|
152
|
+
updated: {
|
|
153
|
+
timestamp: assistant.meta.startTime.timestamp,
|
|
154
|
+
timestampUNIX: assistant.meta.startTime.timestampUNIX,
|
|
194
155
|
},
|
|
195
156
|
},
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
157
|
+
},
|
|
158
|
+
metadata: Manager.Metadata().set({ tag: 'user/oauth2' }),
|
|
159
|
+
}, { merge: true });
|
|
160
|
+
|
|
161
|
+
// Delete CSRF token (cleanup)
|
|
162
|
+
await usageDocRef.update({
|
|
163
|
+
[`oauth2.${stateData.provider}`]: admin.firestore.FieldValue.delete(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assistant.log('OAuth2 tokenize complete');
|
|
201
167
|
|
|
202
168
|
return assistant.respond({ success: true });
|
|
203
169
|
}
|
|
204
170
|
|
|
205
|
-
async function processRefresh(assistant, Manager,
|
|
206
|
-
|
|
171
|
+
async function processRefresh({ assistant, Manager, user, settings, libraries }) {
|
|
172
|
+
const context = await buildContext({ assistant, Manager, user, settings, libraries });
|
|
173
|
+
|
|
174
|
+
if (context.error) {
|
|
175
|
+
return assistant.respond(context.error.message, { code: context.error.code });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { admin, oauth2Provider, targetUid, targetUser, clientId, clientSecret } = context;
|
|
207
179
|
|
|
208
|
-
const
|
|
180
|
+
const refreshToken = targetUser?.oauth2?.[settings.provider]?.token?.refresh_token;
|
|
209
181
|
|
|
210
|
-
if (!
|
|
182
|
+
if (!refreshToken) {
|
|
211
183
|
return assistant.respond('No refresh token found', { code: 400 });
|
|
212
184
|
}
|
|
213
185
|
|
|
214
186
|
const body = {
|
|
215
|
-
client_id,
|
|
216
|
-
client_secret,
|
|
187
|
+
client_id: clientId,
|
|
188
|
+
client_secret: clientSecret,
|
|
217
189
|
grant_type: 'refresh_token',
|
|
218
|
-
refresh_token,
|
|
190
|
+
refresh_token: refreshToken,
|
|
219
191
|
};
|
|
220
192
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const refreshResponse = await fetch(oauth2.urls.refresh, {
|
|
193
|
+
const refreshResponse = await fetch(oauth2Provider.urls.refresh, {
|
|
224
194
|
method: 'POST',
|
|
225
195
|
timeout: 60000,
|
|
226
196
|
response: 'json',
|
|
227
|
-
tries: 1,
|
|
228
|
-
log: true,
|
|
229
197
|
body: new URLSearchParams(body),
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
233
|
-
},
|
|
234
|
-
}).catch((e) => e);
|
|
235
|
-
|
|
236
|
-
assistant.log('refreshResponse', refreshResponse);
|
|
198
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
199
|
+
}).catch(e => e);
|
|
237
200
|
|
|
238
201
|
if (refreshResponse instanceof Error) {
|
|
239
|
-
return assistant.respond(refreshResponse.message
|
|
202
|
+
return assistant.respond(`Token refresh failed: ${refreshResponse.message}`, { code: 500 });
|
|
240
203
|
}
|
|
241
204
|
|
|
242
|
-
//
|
|
243
|
-
await admin.firestore().doc(`users/${
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
205
|
+
// Update stored tokens
|
|
206
|
+
await admin.firestore().doc(`users/${targetUid}`).set({
|
|
207
|
+
oauth2: {
|
|
208
|
+
[settings.provider]: {
|
|
209
|
+
token: {
|
|
210
|
+
access_token: refreshResponse.access_token,
|
|
211
|
+
refresh_token: refreshResponse.refresh_token || refreshToken, // Some providers don't return new refresh token
|
|
212
|
+
token_type: refreshResponse.token_type,
|
|
213
|
+
expires_in: refreshResponse.expires_in,
|
|
214
|
+
scope: refreshResponse.scope,
|
|
252
215
|
},
|
|
253
|
-
},
|
|
254
|
-
metadata: Manager.Metadata().set({ tag: 'user/oauth2' }),
|
|
255
|
-
}, { merge: true })
|
|
256
|
-
.catch((e) => {
|
|
257
|
-
return assistant.respond(`Failed to store tokens: ${e.message}`, { code: 500 });
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
return assistant.respond({ success: true });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function processDeauthorize(assistant, Manager, admin, settings, uid) {
|
|
264
|
-
await admin.firestore().doc(`users/${uid}`)
|
|
265
|
-
.set({
|
|
266
|
-
oauth2: {
|
|
267
|
-
[settings.provider]: {},
|
|
268
216
|
updated: {
|
|
269
217
|
timestamp: assistant.meta.startTime.timestamp,
|
|
270
218
|
timestampUNIX: assistant.meta.startTime.timestampUNIX,
|
|
271
219
|
},
|
|
272
220
|
},
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return assistant.respond(`Failed to deauthorize: ${e.message}`, { code: 500 });
|
|
277
|
-
});
|
|
221
|
+
},
|
|
222
|
+
metadata: Manager.Metadata().set({ tag: 'user/oauth2' }),
|
|
223
|
+
}, { merge: true });
|
|
278
224
|
|
|
279
225
|
return assistant.respond({ success: true });
|
|
280
226
|
}
|
|
281
|
-
|
|
282
|
-
async function processStatus(assistant, Manager, admin, settings, oauth2, uid, userData) {
|
|
283
|
-
const removeInvalidTokens = settings.removeInvalidTokens;
|
|
284
|
-
|
|
285
|
-
const token = _.get(userData, `oauth2.${settings.provider}.token.refresh_token`, '');
|
|
286
|
-
|
|
287
|
-
if (!token) {
|
|
288
|
-
return assistant.respond({ status: 'disconnected' });
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// If provider has verifyConnection, use it
|
|
292
|
-
if (oauth2.verifyConnection) {
|
|
293
|
-
const status = await oauth2.verifyConnection(token, Manager, assistant)
|
|
294
|
-
.catch(async (e) => {
|
|
295
|
-
if (removeInvalidTokens) {
|
|
296
|
-
await removeOAuth2Token(admin, settings.provider, uid, assistant, Manager);
|
|
297
|
-
}
|
|
298
|
-
return 'error';
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
if (status === 'disconnected' && removeInvalidTokens) {
|
|
302
|
-
await removeOAuth2Token(admin, settings.provider, uid, assistant, Manager);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return assistant.respond({ status });
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Default to connected if we have a token
|
|
309
|
-
return assistant.respond({ status: 'connected' });
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function removeOAuth2Token(admin, provider, uid, assistant, Manager) {
|
|
313
|
-
await admin.firestore().doc(`users/${uid}`)
|
|
314
|
-
.set({
|
|
315
|
-
oauth2: {
|
|
316
|
-
[provider]: {},
|
|
317
|
-
updated: {
|
|
318
|
-
timestamp: assistant.meta.startTime.timestamp,
|
|
319
|
-
timestampUNIX: assistant.meta.startTime.timestampUNIX,
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
metadata: Manager.Metadata().set({ tag: 'user/oauth2' }),
|
|
323
|
-
}, { merge: true });
|
|
324
|
-
|
|
325
|
-
assistant.log(`Removed disconnected token for user: ${uid}`);
|
|
326
|
-
}
|
|
@@ -7,13 +7,36 @@ module.exports = {
|
|
|
7
7
|
authorize: 'https://discord.com/api/oauth2/authorize',
|
|
8
8
|
tokenize: 'https://discord.com/api/oauth2/token',
|
|
9
9
|
refresh: 'https://discord.com/api/oauth2/token',
|
|
10
|
+
revoke: 'https://discord.com/api/oauth2/token/revoke',
|
|
10
11
|
status: '',
|
|
11
12
|
removeAccess: 'https://discord.com/channels/@me',
|
|
12
13
|
},
|
|
14
|
+
scope: ['identify', 'email'],
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// Discord doesn't need special auth params
|
|
17
|
+
authParams: {},
|
|
18
|
+
|
|
19
|
+
// Revoke a token with Discord
|
|
20
|
+
async revokeToken(token, context) {
|
|
21
|
+
const { assistant, clientId, clientSecret } = context;
|
|
22
|
+
|
|
23
|
+
const response = await fetch(this.urls.revoke, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
timeout: 30000,
|
|
26
|
+
body: new URLSearchParams({
|
|
27
|
+
token,
|
|
28
|
+
client_id: clientId,
|
|
29
|
+
client_secret: clientSecret,
|
|
30
|
+
}),
|
|
31
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
32
|
+
}).catch(e => e);
|
|
33
|
+
|
|
34
|
+
if (response instanceof Error) {
|
|
35
|
+
assistant.log('Discord revokeToken error:', response.message);
|
|
36
|
+
return { revoked: false, reason: response.message };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { revoked: true };
|
|
17
40
|
},
|
|
18
41
|
|
|
19
42
|
async verifyIdentity(tokenizeResult, Manager, assistant) {
|
|
@@ -8,13 +8,36 @@ module.exports = {
|
|
|
8
8
|
authorize: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
9
9
|
tokenize: 'https://oauth2.googleapis.com/token',
|
|
10
10
|
refresh: 'https://oauth2.googleapis.com/token',
|
|
11
|
+
revoke: 'https://oauth2.googleapis.com/revoke',
|
|
11
12
|
status: 'https://oauth2.googleapis.com/tokeninfo',
|
|
12
13
|
removeAccess: 'https://myaccount.google.com/security',
|
|
13
14
|
},
|
|
15
|
+
scope: ['openid', 'email', 'profile'],
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
// Google-specific OAuth parameters
|
|
18
|
+
authParams: {
|
|
19
|
+
access_type: 'offline',
|
|
20
|
+
prompt: 'consent',
|
|
21
|
+
include_granted_scopes: 'true',
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Revoke a token with Google
|
|
25
|
+
async revokeToken(token, context) {
|
|
26
|
+
const { assistant } = context;
|
|
27
|
+
|
|
28
|
+
const response = await fetch(this.urls.revoke, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
timeout: 30000,
|
|
31
|
+
body: new URLSearchParams({ token }),
|
|
32
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
33
|
+
}).catch(e => e);
|
|
34
|
+
|
|
35
|
+
if (response instanceof Error) {
|
|
36
|
+
assistant.log('Google revokeToken error:', response.message);
|
|
37
|
+
return { revoked: false, reason: response.message };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { revoked: true };
|
|
18
41
|
},
|
|
19
42
|
|
|
20
43
|
async verifyIdentity(tokenizeResult, Manager, assistant) {
|
|
@@ -24,6 +47,11 @@ module.exports = {
|
|
|
24
47
|
const decoded = jwtDecode(tokenizeResult.id_token);
|
|
25
48
|
assistant.log('verifyIdentity(): decoded', decoded);
|
|
26
49
|
|
|
50
|
+
// Require email scope for proper identity verification
|
|
51
|
+
if (!decoded.email) {
|
|
52
|
+
throw new Error('Email scope is required. Please ensure "email" scope is included in the OAuth request.');
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
// Check if exists
|
|
28
56
|
const snap = await Manager.libraries.admin.firestore().collection('users')
|
|
29
57
|
.where('oauth2.google.identity.email', '==', decoded.email)
|
|
@@ -32,10 +32,11 @@ module.exports = async ({ assistant, user, settings, libraries }) => {
|
|
|
32
32
|
count += await signOutOfSession(admin, assistant, uid, 'gatherings/online');
|
|
33
33
|
|
|
34
34
|
// Revoke Firebase refresh tokens
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
});
|
|
35
|
+
try {
|
|
36
|
+
await admin.auth().revokeRefreshTokens(uid);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return assistant.respond(`Failed to sign out of all sessions: ${e}`, { code: 500 });
|
|
39
|
+
}
|
|
39
40
|
|
|
40
41
|
return assistant.respond({
|
|
41
42
|
sessions: count,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module.exports = ({ user }) => ({
|
|
2
|
+
uid: {
|
|
3
|
+
types: ['string'],
|
|
4
|
+
default: user?.auth?.uid,
|
|
5
|
+
required: false,
|
|
6
|
+
},
|
|
7
|
+
provider: {
|
|
8
|
+
types: ['string'],
|
|
9
|
+
required: true,
|
|
10
|
+
},
|
|
11
|
+
action: {
|
|
12
|
+
types: ['string'],
|
|
13
|
+
default: 'authorize',
|
|
14
|
+
enum: ['authorize', 'status'],
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
redirect: {
|
|
18
|
+
types: ['boolean'],
|
|
19
|
+
default: true,
|
|
20
|
+
required: false,
|
|
21
|
+
},
|
|
22
|
+
removeInvalidTokens: {
|
|
23
|
+
types: ['boolean'],
|
|
24
|
+
default: true,
|
|
25
|
+
required: false,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -6,72 +6,20 @@ module.exports = ({ user }) => ({
|
|
|
6
6
|
},
|
|
7
7
|
provider: {
|
|
8
8
|
types: ['string'],
|
|
9
|
-
|
|
10
|
-
required: true,
|
|
9
|
+
required: false, // Not required for tokenize (provider comes from encrypted state)
|
|
11
10
|
},
|
|
12
|
-
|
|
11
|
+
action: {
|
|
13
12
|
types: ['string'],
|
|
14
|
-
default: '
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
redirect: {
|
|
18
|
-
types: ['boolean'],
|
|
19
|
-
default: true,
|
|
20
|
-
required: false,
|
|
21
|
-
},
|
|
22
|
-
referrer: {
|
|
23
|
-
types: ['string'],
|
|
24
|
-
default: undefined,
|
|
25
|
-
required: false,
|
|
26
|
-
},
|
|
27
|
-
serverUrl: {
|
|
28
|
-
types: ['string'],
|
|
29
|
-
default: undefined,
|
|
30
|
-
required: false,
|
|
31
|
-
},
|
|
32
|
-
redirect_uri: {
|
|
33
|
-
types: ['string'],
|
|
34
|
-
default: undefined,
|
|
35
|
-
required: false,
|
|
36
|
-
},
|
|
37
|
-
scope: {
|
|
38
|
-
types: ['array', 'string'],
|
|
39
|
-
default: [],
|
|
13
|
+
default: 'tokenize',
|
|
14
|
+
enum: ['tokenize', 'refresh'],
|
|
40
15
|
required: false,
|
|
41
16
|
},
|
|
42
17
|
code: {
|
|
43
18
|
types: ['string'],
|
|
44
|
-
|
|
45
|
-
required: false,
|
|
46
|
-
},
|
|
47
|
-
access_type: {
|
|
48
|
-
types: ['string'],
|
|
49
|
-
default: 'offline',
|
|
50
|
-
required: false,
|
|
51
|
-
},
|
|
52
|
-
prompt: {
|
|
53
|
-
types: ['string'],
|
|
54
|
-
default: 'consent',
|
|
55
|
-
required: false,
|
|
56
|
-
},
|
|
57
|
-
include_granted_scopes: {
|
|
58
|
-
types: ['string', 'boolean'],
|
|
59
|
-
default: 'true',
|
|
60
|
-
required: false,
|
|
61
|
-
},
|
|
62
|
-
response_type: {
|
|
63
|
-
types: ['string'],
|
|
64
|
-
default: 'code',
|
|
65
|
-
required: false,
|
|
19
|
+
required: false, // Required for tokenize
|
|
66
20
|
},
|
|
67
|
-
|
|
68
|
-
types: ['boolean'],
|
|
69
|
-
default: true,
|
|
70
|
-
required: false,
|
|
71
|
-
},
|
|
72
|
-
authenticationToken: {
|
|
21
|
+
encryptedState: {
|
|
73
22
|
types: ['string'],
|
|
74
|
-
|
|
75
|
-
required: false,
|
|
23
|
+
required: false, // Required for tokenize
|
|
76
24
|
},
|
|
77
25
|
});
|