Skip to content

Commit a4ce3ad

Browse files
committed
Added tests, better workflows and some better contributing guidelines
1 parent 5318665 commit a4ce3ad

21 files changed

+1783
-86
lines changed

Diff for: .github/workflows/deploy.yml

+21-1
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,29 @@ jobs:
4949
fi
5050
fi
5151
52+
lint:
53+
runs-on: self-hosted
54+
55+
steps:
56+
- name: Checkout code
57+
uses: actions/checkout@v4
58+
59+
- name: Run eslint
60+
run: npm run lint
61+
62+
test:
63+
runs-on: self-hosted
64+
65+
steps:
66+
- name: Checkout code
67+
uses: actions/checkout@v4
68+
69+
- name: Run tests
70+
run: npm run test
71+
5272
build-and-push:
5373
runs-on: self-hosted
54-
needs: setup
74+
needs: [setup, lint, test]
5575

5676
steps:
5777
- name: Set up Docker Buildx

Diff for: CONTRIBUTING.md

+11
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,14 @@ Contributions are welcome! In order to get started, please fork the repository a
55
## Local Development
66

77
To get started with local development, the [Docker Compose](/docs/compose.md) example is a good place to start. This will allow you to run the authentication proxy locally and test your changes.
8+
9+
## Provider Development
10+
11+
In order to add a new provider, simply create a new file in the `src/providers/` directory. This file should contain an implementation using a [Passport.js](http://www.passportjs.org/) strategy. New providers will automatically be discovered. Look at the existing providers for examples.
12+
13+
Implementations without a corresponding Passport.js strategy may be possible, but is not tested or supported.
14+
15+
Be sure to include unit tests for your provider. These tests should be placed in the `__test__/providers/` directory. Look at the existing tests for examples.
16+
17+
> [!NOTE]
18+
> All provider configuration should be able to be set via environment variables. This allows the application to remain stateless and easily scalable. For some providers, a configuration file may be required. In this case, the volume should be mounted to the container and the path should be able to be set via an environment variable. (defaulting to `/etc/auth/<filename>`)

Diff for: README.md

+5
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ As mentioned above, there are a few environment variables that can be used to co
5555
| SESSION_SECRET | The secret used to sign the session cookie | |
5656
| ACCESS_TOKEN_NAME | The name of the access token cookie | `_access_token` |
5757
| ACCESS_TOKEN_SECRET | The secret used to sign the access token cookie | `secret` |
58+
| ACCESS_TOKEN_EXPIRATION | The expiration time for the access token cookie | `15m` |
5859
| REFRESH_TOKEN_NAME | The name of the refresh token cookie | `_refresh_token` |
5960
| REFRESH_TOKEN_SECRET | The secret used to sign the refresh token cookie | `refresh` |
61+
| REFRESH_TOKEN_EXPIRATION | The expiration time for the refresh token cookie | `7d` |
6062
| COOKIE_SECURE | Whether the cookies should be secure or not | `true` |
6163
| COOKIE_HOSTS | A list of hosts that the authentication proxy is available on | `localhost` |
6264
| COOKIE_HOSTS_USE_ROOT | Whether the base domain should be used as the cookie domain | `false` |
@@ -68,6 +70,9 @@ As mentioned above, there are a few environment variables that can be used to co
6870
| FORM_DISABLE_CREDITS | Whether the credits should be disabled or not | `false` |
6971
| PROMETHEUS_PREFIX | The prefix for the Prometheus metrics endpoint. Since this route is not secured, it should be random | |
7072

73+
> [!NOTE]
74+
> The `(ACCESS|REFRESH)_TOKEN_EXPIRATION` variables should be in the format `<duration><unit>`, where `<duration>` is a number and `<unit>` is one of `s`, `m`, `h`, `d`.
75+
7176
> [!NOTE]
7277
> The `LONG_LIVED_TOKENS` variable should be in the format `name:token,name:token`. These tokens can be found after logging in to the auth service and visiting the `AUTH_HOST`.
7378
> ![LONG_LIVED_TOKENS](docs/images/long-lived-tokens.png)

Diff for: __tests__/app.test.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// tests/app.test.js
2+
const request = require("supertest");
3+
const jwt = require("jsonwebtoken");
4+
const { app, server } = require("../app");
5+
6+
jest.mock("jsonwebtoken");
7+
8+
describe("Authentication Proxy Tests", () => {
9+
describe("Application Routes", () => {
10+
it("should return OK for the health check route", async () => {
11+
const response = await request(app).get("/healthz");
12+
expect(response.statusCode).toBe(200);
13+
expect(response.text).toBe("OK");
14+
});
15+
16+
it("should redirect unauthenticated users to login", async () => {
17+
jwt.verify.mockImplementation(() => {
18+
throw new Error("Invalid token");
19+
});
20+
const response = await request(app).get("/");
21+
expect(response.statusCode).toBe(302);
22+
});
23+
});
24+
});
25+
26+
afterAll(() => {
27+
server.close();
28+
});

Diff for: __tests__/providers/apple.test.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const { server } = require("../../app");
2+
3+
jest.mock("jsonwebtoken", () => ({
4+
decode: jest.fn().mockReturnValue({
5+
sub: "apple123",
6+
7+
}),
8+
}));
9+
10+
describe("Apple Authentication Strategy", () => {
11+
// Mock environment variables
12+
process.env.APPLE_TEST_CLIENT_ID = "test-client-id";
13+
process.env.APPLE_TEST_TEAM_ID = "test-team-id";
14+
process.env.APPLE_TEST_KEY_ID = "test-key-id";
15+
process.env.APPLE_TEST_PRIVATE_KEY_LOCATION = "/path/to/key";
16+
process.env.APPLE_TEST_DOMAIN_WHITELIST = "example.com";
17+
process.env.APPLE_TEST_USER_WHITELIST = "[email protected]";
18+
process.env.APPLE_TEST_DISPLAY_NAME = "Test Apple";
19+
process.env.APPLE_TEST_ICON = "test-icon";
20+
const appleProvider = require("../../src/providers/apple");
21+
22+
it("should correctly configure the Apple strategy", () => {
23+
const provider = appleProvider("test", "TEST");
24+
25+
expect(provider.name).toBe("apple_test");
26+
expect(provider.type).toBe("apple");
27+
expect(provider.params.clientID).toBe("test-client-id");
28+
expect(provider.params.teamID).toBe("test-team-id");
29+
expect(provider.params.keyID).toBe("test-key-id");
30+
expect(provider.params.privateKeyLocation).toBe("/path/to/key");
31+
expect(provider.params.loginURL).toBe("/_apple/test");
32+
expect(provider.params.callbackURL).toBe("/_apple/test/callback");
33+
});
34+
35+
it("should correctly process idToken in verify callback", (done) => {
36+
const provider = appleProvider("test", "TEST");
37+
const mockIdToken = "mockIdToken";
38+
provider.verify({}, null, null, mockIdToken, {}, (err, user) => {
39+
expect(err).toBeNull();
40+
expect(user).toEqual({
41+
42+
strategy: "apple_test",
43+
profile: {
44+
appleId: "apple123",
45+
46+
},
47+
});
48+
done();
49+
});
50+
});
51+
});
52+
53+
afterAll(() => {
54+
server.close();
55+
});

Diff for: __tests__/providers/google.test.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const { server } = require("../../app");
2+
3+
jest.mock("jsonwebtoken");
4+
5+
describe("Google Authentication Strategy", () => {
6+
// Mock environment variables
7+
process.env.GOOGLE_TEST_CLIENT_ID = "test-client-id";
8+
process.env.GOOGLE_TEST_CLIENT_SECRET = "test-client-secret";
9+
process.env.GOOGLE_TEST_SCOPE = "profile email";
10+
process.env.GOOGLE_TEST_DOMAIN_WHITELIST = "example.com";
11+
process.env.GOOGLE_TEST_USER_WHITELIST = "[email protected]";
12+
process.env.GOOGLE_TEST_DISPLAY_NAME = "Test Google";
13+
process.env.GOOGLE_TEST_ICON = "test-icon";
14+
const googleProvider = require("../../src/providers/google");
15+
16+
it("should correctly configure the Google strategy", () => {
17+
const provider = googleProvider("test", "TEST");
18+
19+
expect(provider.name).toBe("google_test");
20+
expect(provider.type).toBe("google");
21+
expect(provider.params.clientID).toBe("test-client-id");
22+
expect(provider.params.clientSecret).toBe("test-client-secret");
23+
expect(provider.params.loginURL).toBe("/_google/test");
24+
expect(provider.params.callbackURL).toBe("/_google/test/callback");
25+
});
26+
27+
it("should correctly process user profile in verify callback", (done) => {
28+
const provider = googleProvider("test", "TEST");
29+
30+
const mockProfile = {
31+
id: "google123",
32+
displayName: "Test User",
33+
emails: [{ value: "[email protected]" }],
34+
};
35+
36+
provider.verify(null, null, mockProfile, (err, user) => {
37+
expect(err).toBeNull();
38+
expect(user).toEqual({
39+
40+
strategy: "google_test",
41+
profile: {
42+
googleId: "google123",
43+
displayName: "Test User",
44+
45+
},
46+
});
47+
done();
48+
});
49+
});
50+
});
51+
52+
afterAll(() => {
53+
server.close();
54+
});

Diff for: __tests__/providers/local.test.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { server } = require("../../app");
2+
3+
jest.mock("jsonwebtoken");
4+
5+
describe("Local Authentication Strategy", () => {
6+
// Mock environment variables
7+
process.env.LOCAL_TEST_USERS = "user:$apr1$PJ3UdeuW$sdScbEB7d/HK0mFIx/oN1.";
8+
const localProvider = require("../../src/providers/local");
9+
10+
it("should authenticate a user with correct credentials", (done) => {
11+
const verifyCallback = localProvider("test", "TEST").verify;
12+
verifyCallback("user", "password", (err, user) => {
13+
expect(err).toBeNull();
14+
expect(user).toBeTruthy();
15+
done();
16+
});
17+
});
18+
19+
it("should reject a user with incorrect credentials", (done) => {
20+
const verifyCallback = localProvider("test", "TEST").verify;
21+
verifyCallback("testuser", "wrongpassword", (err, user) => {
22+
expect(err).toBeNull();
23+
expect(user).toBeFalsy();
24+
done();
25+
});
26+
});
27+
});
28+
29+
afterAll(() => {
30+
server.close();
31+
});

Diff for: __tests__/providers/oauth2.test.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const { server } = require("../../app");
2+
3+
jest.mock("jsonwebtoken");
4+
jest.mock("request");
5+
6+
describe("OAuth2 Authentication Strategy", () => {
7+
// Mock environment variables
8+
process.env.OAUTH2_TEST_AUTH_URL = "https://example.com/auth";
9+
process.env.OAUTH2_TEST_TOKEN_URL = "https://example.com/token";
10+
process.env.OAUTH2_TEST_CLIENT_ID = "test-client-id";
11+
process.env.OAUTH2_TEST_CLIENT_SECRET = "test-client-secret";
12+
process.env.OAUTH2_TEST_DOMAIN_WHITELIST = "example.com";
13+
process.env.OAUTH2_TEST_USER_WHITELIST = "[email protected]";
14+
process.env.OAUTH2_TEST_DISPLAY_NAME = "Test OAuth2";
15+
process.env.OAUTH2_TEST_ICON = "test-icon";
16+
const oauth2Provider = require("../../src/providers/oauth2");
17+
18+
it("should correctly configure the OAuth2 strategy", () => {
19+
const provider = oauth2Provider("test", "TEST");
20+
21+
expect(provider.name).toBe("oauth2_test");
22+
expect(provider.type).toBe("oauth2");
23+
expect(provider.params.clientID).toBe("test-client-id");
24+
expect(provider.params.clientSecret).toBe("test-client-secret");
25+
expect(provider.params.loginURL).toBe("/_oauth2/test");
26+
expect(provider.params.callbackURL).toBe("/_oauth2/test/callback");
27+
});
28+
29+
it("should correctly process user profile in verify callback", (done) => {
30+
const provider = oauth2Provider("test", "TEST");
31+
const mockProfile = { id: "[email protected]", displayName: "Test User" };
32+
33+
provider.verify(null, null, mockProfile, (error, user) => {
34+
expect(error).toBeNull();
35+
expect(user).toEqual({
36+
id: mockProfile.id,
37+
strategy: "oauth2_test",
38+
profile: mockProfile,
39+
});
40+
done();
41+
});
42+
});
43+
});
44+
45+
afterAll(() => {
46+
server.close();
47+
});

Diff for: __tests__/providers/oidc.test.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const { server } = require("../../app");
2+
3+
jest.mock("jsonwebtoken");
4+
5+
describe("OAuth2 Authentication Strategy", () => {
6+
// Mock environment variables
7+
process.env.OIDC_TEST_ISSUER = "https://example.com";
8+
process.env.OIDC_TEST_AUTH_URL = "https://example.com/auth";
9+
process.env.OIDC_TEST_TOKEN_URL = "https://example.com/token";
10+
process.env.OIDC_TEST_USER_URL = "https://example.com/userinfo";
11+
process.env.OIDC_TEST_CLIENT_ID = "test-client-id";
12+
process.env.OIDC_TEST_CLIENT_SECRET = "test-client-secret";
13+
process.env.OIDC_TEST_DOMAIN_WHITELIST = "example.com";
14+
process.env.OIDC_TEST_USER_WHITELIST = "[email protected]";
15+
process.env.OIDC_TEST_DISPLAY_NAME = "Test OIDC";
16+
process.env.OIDC_TEST_ICON = "fas fa-test";
17+
const oidcProvider = require("../../src/providers/oidc");
18+
19+
it("should correctly configure the OIDC strategy", () => {
20+
const provider = oidcProvider("test", "TEST");
21+
22+
expect(provider.name).toBe("oidc_test");
23+
expect(provider.type).toBe("oidc");
24+
expect(provider.params.clientID).toBe("test-client-id");
25+
expect(provider.params.clientSecret).toBe("test-client-secret");
26+
expect(provider.params.loginURL).toBe("/_oidc/test");
27+
expect(provider.params.callbackURL).toBe("/_oidc/test/callback");
28+
});
29+
30+
it("should correctly process user profile in verify callback", (done) => {
31+
const provider = oidcProvider("test", "TEST");
32+
const mockProfile = {
33+
id: "oidc123",
34+
displayName: "Test User",
35+
emails: [{ value: "[email protected]" }],
36+
photos: [{ value: "http://example.com/photo.jpg" }],
37+
};
38+
39+
provider.verify(null, "oidc123", mockProfile, null, null, (err, user) => {
40+
expect(err).toBeNull();
41+
expect(user).toEqual({
42+
43+
strategy: "oidc_test",
44+
profile: {
45+
oidcId: "oidc123",
46+
displayName: "Test User",
47+
48+
photo: "http://example.com/photo.jpg",
49+
},
50+
});
51+
done();
52+
});
53+
});
54+
});
55+
56+
afterAll(() => {
57+
server.close();
58+
});

0 commit comments

Comments
 (0)