@connexa/mcp-oauth-firebase-middleware 1.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/lib/token.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * OAuth Token Endpoint
3
+ * Handles token exchange and refresh flows
4
+ */
5
+
6
+ const { validatePKCE } = require('./pkce');
7
+ const { authenticateUser, refreshTokens } = require('./firebase-client');
8
+ const storage = require('./storage');
9
+
10
+ /**
11
+ * Create token endpoint handler
12
+ */
13
+ function createTokenHandler(config) {
14
+ return async (req, res) => {
15
+ try {
16
+ const { grant_type } = req.body;
17
+
18
+ if (!grant_type) {
19
+ return sendError(res, 400, 'invalid_request', 'Missing grant_type');
20
+ }
21
+
22
+ if (grant_type === 'authorization_code') {
23
+ return handleAuthorizationCodeGrant(req, res, config);
24
+ } else if (grant_type === 'refresh_token') {
25
+ return handleRefreshTokenGrant(req, res, config);
26
+ } else {
27
+ return sendError(res, 400, 'unsupported_grant_type', `Grant type ${grant_type} not supported`);
28
+ }
29
+
30
+ } catch (error) {
31
+ console.error('Token endpoint error:', error);
32
+ return sendError(res, 500, 'server_error', 'Internal server error');
33
+ }
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Handle authorization code grant
39
+ */
40
+ async function handleAuthorizationCodeGrant(req, res, config) {
41
+ const {
42
+ code,
43
+ client_id,
44
+ code_verifier,
45
+ redirect_uri
46
+ } = req.body;
47
+
48
+ // Validate required parameters
49
+ if (!code || !client_id || !code_verifier || !redirect_uri) {
50
+ return sendError(res, 400, 'invalid_request', 'Missing required parameters');
51
+ }
52
+
53
+ // Retrieve and consume authorization code
54
+ const codeData = storage.consume(code);
55
+
56
+ if (!codeData) {
57
+ return sendError(res, 400, 'invalid_grant', 'Invalid or expired authorization code');
58
+ }
59
+
60
+ // Validate client_id matches
61
+ if (codeData.clientId !== client_id) {
62
+ return sendError(res, 400, 'invalid_grant', 'client_id mismatch');
63
+ }
64
+
65
+ // Validate redirect_uri matches
66
+ if (codeData.redirectUri !== redirect_uri) {
67
+ return sendError(res, 400, 'invalid_grant', 'redirect_uri mismatch');
68
+ }
69
+
70
+ // Validate PKCE
71
+ try {
72
+ const isValid = validatePKCE(
73
+ code_verifier,
74
+ codeData.codeChallenge,
75
+ codeData.codeChallengeMethod
76
+ );
77
+
78
+ if (!isValid) {
79
+ return sendError(res, 400, 'invalid_grant', 'PKCE validation failed');
80
+ }
81
+ } catch (error) {
82
+ return sendError(res, 400, 'invalid_grant', error.message);
83
+ }
84
+
85
+ // Re-authenticate with Firebase to get fresh tokens
86
+ // Use stored user credentials
87
+ const email = codeData.userEmail;
88
+ const password = config.clientSecret;
89
+
90
+ let authResult;
91
+ try {
92
+ authResult = await authenticateUser(email, password);
93
+ } catch (error) {
94
+ console.error('Firebase auth error in token exchange:', error.message);
95
+ return sendError(res, 400, 'invalid_grant', 'Failed to issue tokens');
96
+ }
97
+
98
+ // Return tokens
99
+ res.json({
100
+ access_token: authResult.idToken,
101
+ token_type: 'Bearer',
102
+ expires_in: 3600,
103
+ refresh_token: authResult.refreshToken,
104
+ scope: codeData.scope
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Handle refresh token grant
110
+ */
111
+ async function handleRefreshTokenGrant(req, res, config) {
112
+ const {
113
+ refresh_token,
114
+ client_id
115
+ } = req.body;
116
+
117
+ // Validate required parameters
118
+ if (!refresh_token || !client_id) {
119
+ return sendError(res, 400, 'invalid_request', 'Missing required parameters');
120
+ }
121
+
122
+ // Refresh Firebase tokens
123
+ let tokens;
124
+ try {
125
+ tokens = await refreshTokens(refresh_token);
126
+ } catch (error) {
127
+ console.error('Token refresh error:', error.message);
128
+ return sendError(res, 400, 'invalid_grant', 'Invalid refresh token');
129
+ }
130
+
131
+ // Return new tokens
132
+ res.json({
133
+ access_token: tokens.idToken,
134
+ token_type: 'Bearer',
135
+ expires_in: tokens.expiresIn,
136
+ refresh_token: tokens.refreshToken
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Send error response
142
+ */
143
+ function sendError(res, statusCode, error, errorDescription) {
144
+ res.status(statusCode).json({
145
+ error,
146
+ error_description: errorDescription
147
+ });
148
+ }
149
+
150
+ module.exports = { createTokenHandler };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Token Validation Middleware
3
+ * Validates Firebase ID tokens for MCP endpoint protection
4
+ * Supports role-based and permission-based access control via custom claims
5
+ */
6
+
7
+ const { verifyIdToken } = require('../lib/firebase-client');
8
+
9
+ /**
10
+ * Middleware to validate Bearer token in Authorization header
11
+ * @param {Object} options - Optional validation options
12
+ * @param {string} options.role - Required role (checks custom claim)
13
+ * @param {string|string[]} options.permissions - Required permission(s) (checks custom claim)
14
+ * @param {Function} options.customCheck - Custom validation function(decodedToken) => boolean
15
+ */
16
+ function validateToken(options = {}) {
17
+ // Support both validateToken() and validateToken(options)
18
+ return function(req, res, next) {
19
+ const authHeader = req.headers.authorization;
20
+
21
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
22
+ return res.status(401).json({
23
+ error: 'unauthorized',
24
+ error_description: 'Missing or invalid Authorization header'
25
+ }).set('WWW-Authenticate', 'Bearer realm="MCP Server"');
26
+ }
27
+
28
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
29
+
30
+ verifyIdToken(token)
31
+ .then(decodedToken => {
32
+ // Attach user info to request
33
+ req.user = {
34
+ uid: decodedToken.uid,
35
+ email: decodedToken.email,
36
+ emailVerified: decodedToken.email_verified,
37
+ role: decodedToken.role,
38
+ permissions: decodedToken.permissions || [],
39
+ customClaims: decodedToken
40
+ };
41
+
42
+ // Check role requirement
43
+ if (options.role && decodedToken.role !== options.role) {
44
+ return res.status(403).json({
45
+ error: 'insufficient_permissions',
46
+ error_description: `Required role: ${options.role}`
47
+ });
48
+ }
49
+
50
+ // Check permission requirement(s)
51
+ if (options.permissions) {
52
+ const userPermissions = decodedToken.permissions || [];
53
+ const requiredPermissions = Array.isArray(options.permissions)
54
+ ? options.permissions
55
+ : [options.permissions];
56
+
57
+ const hasAllPermissions = requiredPermissions.every(perm =>
58
+ userPermissions.includes(perm)
59
+ );
60
+
61
+ if (!hasAllPermissions) {
62
+ return res.status(403).json({
63
+ error: 'insufficient_permissions',
64
+ error_description: `Required permissions: ${requiredPermissions.join(', ')}`
65
+ });
66
+ }
67
+ }
68
+
69
+ // Custom validation check
70
+ if (options.customCheck && typeof options.customCheck === 'function') {
71
+ try {
72
+ const isValid = options.customCheck(decodedToken);
73
+ if (!isValid) {
74
+ return res.status(403).json({
75
+ error: 'insufficient_permissions',
76
+ error_description: 'Custom authorization check failed'
77
+ });
78
+ }
79
+ } catch (error) {
80
+ console.error('Custom check error:', error.message);
81
+ return res.status(403).json({
82
+ error: 'insufficient_permissions',
83
+ error_description: 'Authorization check failed'
84
+ });
85
+ }
86
+ }
87
+
88
+ next();
89
+ })
90
+ .catch(error => {
91
+ console.error('Token validation failed:', error.message);
92
+ return res.status(401).json({
93
+ error: 'invalid_token',
94
+ error_description: 'Token validation failed'
95
+ }).set('WWW-Authenticate', 'Bearer realm="MCP Server", error="invalid_token"');
96
+ });
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Convenience middleware: Require specific role
102
+ * @param {string} role - Required role name
103
+ */
104
+ function requireRole(role) {
105
+ return validateToken({ role });
106
+ }
107
+
108
+ /**
109
+ * Convenience middleware: Require one or more permissions
110
+ * @param {string|string[]} permissions - Required permission(s)
111
+ */
112
+ function requirePermissions(permissions) {
113
+ return validateToken({ permissions });
114
+ }
115
+
116
+ /**
117
+ * Convenience middleware: Require admin role
118
+ */
119
+ function requireAdmin() {
120
+ return validateToken({ role: 'admin' });
121
+ }
122
+
123
+ module.exports = validateToken;
124
+ module.exports.validateToken = validateToken;
125
+ module.exports.requireRole = requireRole;
126
+ module.exports.requirePermissions = requirePermissions;
127
+ module.exports.requireAdmin = requireAdmin;
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@connexa/mcp-oauth-firebase-middleware",
3
+ "version": "1.0.0",
4
+ "description": "OAuth 2.0 middleware for MCP servers using Firebase Auth backend",
5
+ "private": false,
6
+ "main": "lib/index.js",
7
+ "scripts": {
8
+ "start": "node server.js",
9
+ "dev": "node server.js",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "oauth",
14
+ "oauth2",
15
+ "firebase",
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "authentication",
19
+ "middleware",
20
+ "pkce"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/yourusername/mcp-oauth-firebase-middleware"
27
+ },
28
+ "publishConfig": {
29
+ "access": "restricted"
30
+ },
31
+ "files": [
32
+ "lib/",
33
+ "middleware/",
34
+ "README.md"
35
+ ],
36
+ "dependencies": {
37
+ "firebase": "^10.7.1",
38
+ "firebase-admin": "^12.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "express": "^4.18.0"
42
+ },
43
+ "devDependencies": {
44
+ "express": "^4.18.2",
45
+ "dotenv": "^16.3.1",
46
+ "cors": "^2.8.5"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ }
51
+ }