Skip to content

Commit 126b3db

Browse files
authored
Merge pull request #11 from Creoox/develop
Added support for different token types + authentication during login
2 parents abafa81 + 1799da0 commit 126b3db

File tree

13 files changed

+124
-36
lines changed

13 files changed

+124
-36
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Currently tested providers:
6666
| OIDC_CLIENT_SECRET | string | No | OIDC client secret (if set) |
6767
| OIDC_VERIFICATION_TYPE | string | Yes | 'jwt' - decoding or 'introspection' - asking AS |
6868
| JWT_STRICT_AUDIENCE | boolean | Yes | true if token should be used for strict audinence only |
69+
| JWT_TOKEN_TYPE | string | No | Used token, either 'access_token' (default) or 'id_token' |
6970
| AUTH_ENDPOINT | string | No | Service redirection endpoint, '/\_oauth' by default |
7071
| AUTH_ALLOW_UNSEC_OPTIONS | boolean | No | Allow unsecured OPTIONS request, false by default |
7172
| LOGIN_WHEN_NO_TOKEN | boolean | Yes | true if login functionality should be on (**dev only!**) |
@@ -161,7 +162,7 @@ traefik:
161162
- cx-example-net
162163

163164
traefik-forward-auth:
164-
image: creoox/cx-traefik-forward-auth:1.1.4
165+
image: creoox/cx-traefik-forward-auth:1.1.5
165166
container_name: cx-example-traefik-forward-auth
166167
env_file:
167168
- ./cx-traefik-forward-auth.env

app/.env.example

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## Application settings
22
APP_NAME=cx-traefik-forward-auth
3-
APP_VERSION=1.1.4
3+
APP_VERSION=1.1.5
44
APP_PORT=4181
55

66
## Environment settings
@@ -15,6 +15,7 @@ OIDC_VERIFICATION_TYPE=jwt
1515

1616
## Middelware behaviour settings
1717
JWT_STRICT_AUDIENCE=false
18+
JWT_TOKEN_TYPE=access_token
1819
AUTH_ENDPOINT=/_oauth
1920
AUTH_ALLOW_UNSEC_OPTIONS=true
2021
LOGIN_WHEN_NO_TOKEN=true

app/public/token/index.ejs

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<title>Access Token</title>
5+
<title>Your Token</title>
66

77
<style type="text/css">
88
@import url("https://fonts.googleapis.com/css?family=Lato");
@@ -43,8 +43,8 @@
4343
</head>
4444
<body>
4545
<div class="container" style="text-align: center">
46-
<h1><b>Acess Token</b></h1>
47-
<p><%= access_token %></p>
46+
<h1><b>Your <%= token_type %></b></h1>
47+
<p><%= token %></p>
4848
</div>
4949
</body>
5050
</html>

app/src/app.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
VERIF_TYPE,
88
LOGIN_WHEN_NO_TOKEN,
99
LOGIN_COOKIE_NAME,
10+
JWT_TOKEN_TYPE,
1011
validateDotenvFile,
1112
} from "./models/dotenvModel";
1213
import { checkIfIntrospectionPossible } from "./services/preAuth";
@@ -79,7 +80,7 @@ const isSessionEstablished = (
7980
res: Response,
8081
next: NextFunction
8182
) => {
82-
if (LOGIN_WHEN_NO_TOKEN && !!(req.session as LoginSession).access_token) {
83+
if (LOGIN_WHEN_NO_TOKEN && !!(req.session as LoginSession).token) {
8384
next();
8485
} else {
8586
next("route");
@@ -104,7 +105,8 @@ app.get(
104105
)
105106
) {
106107
res.status(400).render("token/index.ejs", {
107-
access_token: (req.session as LoginSession).access_token,
108+
token_type: JWT_TOKEN_TYPE,
109+
token: (req.session as LoginSession).token,
108110
});
109111
return;
110112
}

app/src/models/dotenvModel.ts

+13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const LOGIN_AUTH_FLOW = ((): "implicit" | "code" => {
2424
return "code";
2525
}
2626
})();
27+
export const JWT_TOKEN_TYPE = process.env.JWT_TOKEN_TYPE || "access_token";
2728

2829
const boolTypes = [
2930
true,
@@ -56,6 +57,7 @@ const dotenvVars_optionalStr = [
5657
"AUTH_ALLOW_UNSEC_OPTIONS",
5758
"AUTH_ROLES_STRUCT",
5859
"AUTH_ROLE_NAME",
60+
"JWT_TOKEN_TYPE",
5961
] as const;
6062
const dotenvVars_optionalNum = ["APP_PORT"] as const;
6163

@@ -157,5 +159,16 @@ export function validateDotenvFile(): void {
157159
);
158160
}
159161

162+
// Check if token type is of a valid type
163+
if (
164+
process.env["JWT_TOKEN_TYPE"] &&
165+
process.env["JWT_TOKEN_TYPE"] !== "access_token" &&
166+
process.env["JWT_TOKEN_TYPE"] !== "id_token"
167+
) {
168+
throw new Error(
169+
"Variable: JWT_TOKEN_TYPE has to be either 'access_token' (default) or 'id_token'"
170+
);
171+
}
172+
160173
return;
161174
}

app/src/models/loginModel.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import session from "express-session";
22

33
export interface LoginSession extends Partial<session.SessionData> {
4-
access_token?: string;
4+
token?: string;
55
}
66

77
export interface LoginCache extends Partial<session.SessionData> {

app/src/services/auth.ts

+37-22
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ import { getJwkKeys, getProviderEndpoints } from "./preAuth";
1010
import { AUTH_ENDPOINT, getOidcClient } from "../states/clients";
1111
import { getLoginCache } from "../states/cache";
1212
import { logger } from "./logger";
13-
import { getRandomString } from "./helpers";
13+
import { decodeB64, getRandomString } from "./helpers";
1414
import type { ActiveOidcToken, InactiveOidcToken } from "../models/authModel";
1515
import type { LoginSession, LoginCache } from "../models/loginModel";
16-
import { LOGIN_AUTH_FLOW, LOGIN_SCOPE } from "../models/dotenvModel";
16+
import {
17+
LOGIN_AUTH_FLOW,
18+
LOGIN_SCOPE,
19+
JWT_TOKEN_TYPE,
20+
} from "../models/dotenvModel";
21+
import { validateTokenRoles } from "./postAuth";
1722

1823
/* eslint-disable @typescript-eslint/no-non-null-assertion */
1924
const JWT_STRICT_AUDIENCE = ["true", "True", "1"].includes(
@@ -114,35 +119,45 @@ export const handleCallback = async (
114119
{ code_verifier: cache.code_verifier, state: params.state }
115120
);
116121
}
122+
// for some reason we have make a copy
123+
const token = JSON.parse(JSON.stringify(tokenSet[JWT_TOKEN_TYPE] as string));
117124

118125
// create login session
119126
req.session.regenerate((err) => {
120127
if (err) {
121128
next(err);
122129
}
123-
(req.session as LoginSession).access_token = tokenSet.access_token;
124-
req.session.save((err) => {
125-
if (err) {
126-
return next(err);
127-
}
128-
if (req.headers["x-forwarded-uri"]) {
129-
const originSchema = req.headers["x-forwarded-proto"];
130-
const originHost = req.headers["x-forwarded-host"];
131-
const originUri = req.headers["x-forwarded-uri"];
132-
const url = `${originSchema}://${originHost}${originUri}`;
133-
res.redirect(url);
134-
} else {
135-
return next(new Error("Missing `X-Forwarded-Uri` Header"));
136-
}
137-
});
130+
try {
131+
const payload = JSON.parse(decodeB64(token.split(".")[1]));
132+
validateTokenRoles(payload);
133+
(req.session as LoginSession).token = tokenSet[JWT_TOKEN_TYPE] as string;
134+
135+
req.session.save((err) => {
136+
if (err) {
137+
return next(err);
138+
}
139+
if (req.headers["x-forwarded-uri"]) {
140+
const originSchema = req.headers["x-forwarded-proto"];
141+
const originHost = req.headers["x-forwarded-host"];
142+
const originUri = req.headers["x-forwarded-uri"];
143+
const url = `${originSchema}://${originHost}${originUri}`;
144+
res.redirect(url);
145+
} else {
146+
return next(new Error("Missing `X-Forwarded-Uri` Header"));
147+
}
148+
});
149+
} catch (err) {
150+
// return Promise.reject(`${err}`);
151+
res.status(403).send(`${err}`);
152+
}
138153
});
139154
};
140155

141156
/**
142157
* Verify token via JWT - decode it using providers JWK Keys.
143158
*
144-
* @param token JWT access_token
145-
* @returns decoded access_token payload
159+
* @param token JWT token (access_token or id_token)
160+
* @returns decoded token payload
146161
*/
147162
export const verifyTokenViaJwt = async (token: string): Promise<JWTPayload> => {
148163
if (!token.includes(".")) {
@@ -165,11 +180,11 @@ export const verifyTokenViaJwt = async (token: string): Promise<JWTPayload> => {
165180
};
166181

167182
/**
168-
* Verify token via Token Introspection - validate access_token on the
183+
* Verify token via Token Introspection - validate token on the
169184
* provider authorization server.
170185
*
171-
* @param token JWT access_token
172-
* @returns decoded access_token payload
186+
* @param token JWT token (access_token or id_token)
187+
* @returns decoded token payload
173188
*/
174189
export const verifyTokenViaIntrospection = async (token: string) => {
175190
if (!token.includes(".")) {

app/src/services/helpers.ts

+9
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,12 @@ export const getStateParam = (url: string, authEndpoint: string): string => {
5656
)[0]
5757
.replace("state=", "");
5858
};
59+
60+
/**
61+
* Decodes b64 strings.
62+
*
63+
* @param str base64-encoded string
64+
* @returns decoded string
65+
*/
66+
export const decodeB64 = (str: string): string =>
67+
Buffer.from(str, "base64").toString("binary");

app/src/services/postAuth.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { ActiveOidcToken, InactiveOidcToken } from "../models/authModel";
1+
import {
2+
ActiveOidcToken,
3+
InactiveOidcToken,
4+
OidcTokenCorePayload,
5+
} from "../models/authModel";
26
import { logger } from "./logger";
37

48
/**
5-
* Verifies token payload (authorizes caller). For the time being it's
6-
* open-to-implementation feature.
9+
* Verifies token payload (checks whether token is active).
710
*
8-
* @param payload
11+
* @param payload token payload
912
* @throws Error if token is inactive
10-
* @throws Error if authorization group is missing in token payload
1113
*/
1214
export function validateTokenPayload<
1315
T extends ActiveOidcToken,
@@ -17,6 +19,18 @@ export function validateTokenPayload<
1719
throw new Error("Token is inactive.");
1820
}
1921

22+
return validateTokenRoles(payload as Partial<T>);
23+
}
24+
25+
/**
26+
* Verifies token payload (authorizes caller).
27+
*
28+
* @param payload token payload
29+
* @throws Error if authorization group is missing in token payload
30+
*/
31+
export function validateTokenRoles<T extends OidcTokenCorePayload>(
32+
payload: Partial<T>
33+
): void {
2034
const authGroupName = process.env.AUTH_ROLE_NAME;
2135
if (authGroupName) {
2236
const groupStruct = process.env.AUTH_ROLES_STRUCT?.split(

app/tests/models/dotenvModel.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,26 @@ describe("Validator for dotenv files", () => {
235235
process.env.AUTH_ROLE_NAME = "dummy-group";
236236
expect(() => validateDotenvFile()).not.toThrow(Error);
237237
});
238+
239+
it("fails if the `JWT_TOKEN_TYPE` is of a wrong type.", () => {
240+
process.env = {
241+
...process.env,
242+
...stringifyObjectValues(dotenvFile),
243+
};
244+
process.env.JWT_TOKEN_TYPE = "dummy_type";
245+
expect(() => validateDotenvFile()).toThrow(Error);
246+
});
247+
248+
it("passes valid `JWT_TOKEN_TYPE` types.", () => {
249+
process.env = {
250+
...process.env,
251+
...stringifyObjectValues(dotenvFile),
252+
};
253+
process.env.JWT_TOKEN_TYPE = "id_token";
254+
expect(() => validateDotenvFile()).not.toThrow(Error);
255+
process.env.JWT_TOKEN_TYPE = "access_token";
256+
expect(() => validateDotenvFile()).not.toThrow(Error);
257+
process.env.JWT_TOKEN_TYPE = undefined;
258+
expect(() => validateDotenvFile()).not.toThrow(Error);
259+
});
238260
});

app/tests/services/helpers.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
getRandomString,
55
getEnvInfo,
66
getStateParam,
7+
decodeB64,
78
} from "../../src/services/helpers";
9+
import { demoUserToken } from "../testData";
810

911
describe("Random string generator", () => {
1012
it("generates different strings", () => {
@@ -47,3 +49,9 @@ describe("State Parameter", () => {
4749
).toEqual(testState);
4850
});
4951
});
52+
53+
describe("base64 decoder", () => {
54+
it("decodes token", () => {
55+
expect(() => decodeB64(demoUserToken)).not.toThrow(Error);
56+
});
57+
});

app/tests/testData.ts

+3
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,6 @@ export const testTokenPayload: Required<OidcTokenCorePayload> & any = {
9595
preferred_username: "Dummy",
9696
9797
};
98+
99+
export const demoUserToken =
100+
"eyJhdF9oYXNoIjoia2xkSHlCUm8tekRFbnRjZURpQ0F3ZyIsInN1YiI6ImRlbW8udXNlckBjcmVvb3guY29tIiwiaXNzIjoiaHR0cHM6Ly9zZXJ2aWNlcy11YXQudWdmaXNjaGVyLmNvbTo0NDMvYXV0aG9yaXphdGlvbnNlcnZlciIsImdyb3VwcyI6WyJGSVdFIiwid2Vic2VydmljZWdyb3VwIiwicmV0YWlsLWNvbm5lY3QtY2FydCIsIkZJV0UtREUiLCJkZWZhdWx0QjJCVW5pdCIsIkdST1VQLXBsYW5uZXIiLCJBQ0NFU1MtUFJJVklMRUdFUyIsImIyYmdyb3VwIiwiYjJiY3VzdG9tZXJncm91cCIsImN1c3RvbWVyZ3JvdXAiLCJGSVdFLVpaLWZpeHBlcmllbmNlIiwiRklXRS1aWi1MTVMiLCJHUk9VUC1mcm9udGVuZC1yZWdpc3RyYXRpb24tc2VsZWN0YWJsZSJdLCJub25jZSI6Ii13MC1MVEJRRzBCVHJmdk9kcW55bDJpbGpnMFhVanViZ0F2Uk02dUhLWm8iLCJhdWQiOiJmaXdlLXp6LWluc3RhbGxmaXgiLCJjb3VudHJ5SXNvIjoiREUiLCJzY29wZSI6WyJvcGVuaWQiXSwibmFtZSI6IkRlbW8gVXNlciIsInN0YXRlIjoiNWlKYXQ4V29EeWZDM3hqenFiODI0dU14aEpYcC1sSVRtUmZaTjJUUE9oMCIsImV4cCI6MTY4ODE1NzQzOSwiaWF0IjoxNjg4MDcxMDM5LCJlbWFpbCI6ImRlbW8udXNlckBjcmVvb3guY29tIn0";

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ services:
88
dockerfile: ./.docker/${ENVIRONMENT}.Dockerfile
99
args:
1010
- node_env_type=${ENVIRONMENT}
11-
# platform: linux/amd64
11+
platform: linux/amd64
1212
image: creoox/cx-traefik-forward-auth:${APP_VERSION}
1313
container_name: ${APP_NAME}-${APP_VERSION}
1414
env_file:

0 commit comments

Comments
 (0)