@cloudron/tegel 1.1.4 → 1.1.6

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.
Files changed (3) hide show
  1. package/index.js +35 -4
  2. package/package.json +2 -2
  3. package/src/oidc.js +24 -10
package/index.js CHANGED
@@ -3,8 +3,7 @@ import fs from 'node:fs';
3
3
  import crypto from 'node:crypto';
4
4
  import session from 'express-session';
5
5
  import FileStoreFactory from 'session-file-store';
6
- import lastMile from '@cloudron/connect-lastmile';
7
- import { HttpError } from '@cloudron/connect-lastmile';
6
+ import { lastMile, HttpError } from '@cloudron/connect-lastmile';
8
7
  import * as oidc from './src/oidc.js';
9
8
 
10
9
  export async function createExpressApp({ oidcConfig, jsonBodySizeLimit = '25mb' }) {
@@ -133,10 +132,35 @@ export function logout(redirectTo) {
133
132
  };
134
133
  }
135
134
 
136
- export function requireAuth(redirectTo = '') {
135
+ export function requireAuth(redirectTo = '', { requiredScopes = [] } = {}) {
137
136
  if (typeof redirectTo !== 'string') throw new Error('requireAuth needs a redirectTo path as non-empty string');
138
137
 
139
- return (req, res, next) => {
138
+ return async (req, res, next) => {
139
+ const accessToken = extractAccessToken(req);
140
+
141
+ if (accessToken) {
142
+ try {
143
+ const introspection = await oidc.introspectToken(accessToken);
144
+
145
+ if (!introspection.active) return next(new HttpError(401, 'Token is not active'));
146
+
147
+ if (requiredScopes.length > 0) {
148
+ const grantedScopes = (introspection.scope || '').split(' ');
149
+ const missingScopes = requiredScopes.filter(s => !grantedScopes.includes(s));
150
+ if (missingScopes.length > 0) return next(new HttpError(403, `Missing required scopes: ${missingScopes.join(', ')}`));
151
+ }
152
+
153
+ req.user = {
154
+ ...introspection,
155
+ username: introspection.sub || introspection.username,
156
+ };
157
+ return next();
158
+ } catch (error) {
159
+ console.error('Token introspection error:', error);
160
+ return next(new HttpError(401, 'Token introspection failed'));
161
+ }
162
+ }
163
+
140
164
  if (req.session && req.session.user) {
141
165
  req.user = req.session.user;
142
166
  return next();
@@ -147,3 +171,10 @@ export function requireAuth(redirectTo = '') {
147
171
  res.redirect(redirectTo);
148
172
  };
149
173
  }
174
+
175
+ function extractAccessToken(req) {
176
+ const authHeader = req.get('Authorization');
177
+ if (authHeader && authHeader.startsWith('Bearer ')) return authHeader.slice(7);
178
+ if (req.query && req.query.accessToken) return req.query.accessToken;
179
+ return null;
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudron/tegel",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "",
5
5
  "license": "GPL-2.0",
6
6
  "author": "Cloudron Developers",
@@ -12,7 +12,7 @@
12
12
  "lint:fix": "eslint . --fix"
13
13
  },
14
14
  "dependencies": {
15
- "@cloudron/connect-lastmile": "^2.3.0",
15
+ "@cloudron/connect-lastmile": "^3.0.0",
16
16
  "express": "^5.2.1",
17
17
  "express-session": "^1.19.0",
18
18
  "openid-client": "^6.8.2",
package/src/oidc.js CHANGED
@@ -2,6 +2,7 @@ import * as client from 'openid-client';
2
2
 
3
3
  let clientConfig = null;
4
4
  let redirectUri = null;
5
+ let clientScope = 'openid profile email';
5
6
 
6
7
  /**
7
8
  * Initialize the OIDC client by discovering the issuer
@@ -10,11 +11,14 @@ export async function initOIDC({
10
11
  issuer = process.env.CLOUDRON_OIDC_ISSUER,
11
12
  clientId = process.env.CLOUDRON_OIDC_CLIENT_ID,
12
13
  clientSecret = process.env.CLOUDRON_OIDC_CLIENT_SECRET,
13
- callbackUrl = (process.env.CLOUDRON_APP_ORIGIN || 'http://localhost:3000') + '/auth/callback'
14
+ callbackUrl = (process.env.CLOUDRON_APP_ORIGIN || 'http://localhost:3000') + '/auth/callback',
15
+ extraScope = '',
14
16
  }) {
15
17
  const issuerUrl = new URL(issuer);
16
18
  redirectUri = new URL(callbackUrl);
17
19
 
20
+ clientScope = `${clientScope} ${extraScope}`;
21
+
18
22
  clientConfig = await client.discovery(
19
23
  issuerUrl,
20
24
  clientId,
@@ -40,7 +44,7 @@ export async function getAuthorizationUrl(req) {
40
44
 
41
45
  const parameters = {
42
46
  redirect_uri: redirectUri,
43
- scope: 'openid profile email',
47
+ scope: clientScope,
44
48
  code_challenge: codeChallenge,
45
49
  code_challenge_method: 'S256',
46
50
  state
@@ -84,18 +88,28 @@ export async function handleCallback(req) {
84
88
  );
85
89
  }
86
90
 
91
+ console.log(tokens)
92
+
87
93
  // Clean up session OIDC state
88
94
  delete req.session.oidc;
89
95
 
96
+ // give common props a nicer name but otherwise return full userInfo as it may have other scopes
97
+ userInfo.username = userInfo.sub;
98
+ userInfo.username = userInfo.sub;
99
+ userInfo.familyName = userInfo.family_name;
100
+ userInfo.givenName = userInfo.given_name;
101
+ userInfo.displayName = userInfo.name;
102
+
90
103
  return {
91
104
  tokens,
92
- user: {
93
- username: userInfo.sub,
94
- email: userInfo.email,
95
- familyName: userInfo.family_name,
96
- givenName: userInfo.given_name,
97
- displayName: userInfo.name,
98
- picture: userInfo.picture
99
- }
105
+ user: userInfo,
100
106
  };
101
107
  }
108
+
109
+ /**
110
+ * Introspect an access token via the OIDC provider's token introspection endpoint (RFC 7662)
111
+ */
112
+ export async function introspectToken(token) {
113
+ if (!clientConfig) throw new Error('call initOIDC() first');
114
+ return client.tokenIntrospection(clientConfig, token);
115
+ }