Skip to content

Commit c8b18bf

Browse files
feat(sso): sso support added (#614)
* deps added, env updated * bootstrap module * models updated * tests for model and factory method added * rm redunant test cases * Update .nvmrc * Refactor SAML validation logic and add unit tests Moved SAML audience, recipient, and time condition validation functions from SamlService to a new utils module for better separation of concerns. Added comprehensive unit tests for these utility functions and for SAML service logic. Improved test data isolation by introducing a unique test string generator. Updated existing user and usersFactory tests to use the new generator and ensure test isolation. Also, prevented MongoDB metrics setup in test environments. * rm try-catch from tests * Refactor SAML response validation to use node-saml Replaces custom SAML assertion validation logic with @node-saml/node-saml for signature, audience, and time validation. Updates error handling to map node-saml errors to SamlValidationError types, adds fallback error type, and removes now-unnecessary utility functions and tests. Extends and improves test coverage for SAML response parsing, error cases, and attribute extraction. * Implement SAML AuthnRequest generation and tests Added logic to generate SAML AuthnRequest using node-saml, extract the request ID from the encoded request, and handle errors. Updated and expanded unit tests to cover successful generation, error cases, and correct invocation of SAML library methods. * SamlStateStore implemetation * Implement SAML SSO controller and tests Added SAML SSO login and ACS endpoint logic to the controller, including user provisioning and session creation. Updated Jest config to use a dedicated test tsconfig. Added comprehensive tests for SAML controller behavior and created a test tsconfig.json. * Add SSO config support with admin-only GraphQL directive Introduces a new @definedOnlyForAdmins directive to restrict certain fields to workspace admins, returning null for non-admins. Adds SSO configuration types, inputs, and resolvers to the workspace schema, including the sso field and updateWorkspaceSso mutation, both protected for admin access. Updates schema wiring to register the new directive and its transformer. * Add dynamic Node version and improve SAML SSO error handling Dockerfiles and GitHub Actions workflow now use a dynamic Node.js version via build args, reading from .nvmrc for consistency. SAML SSO controller adds workspace ID validation, improved error handling, and clearer error responses for SSO initiation and ACS callback. Also documents REDIS_URL in environment types. * Update build-and-push-docker-image.yml * Update build-and-push-docker-image.yml * Update mongodb.ts * Update controller.test.ts * Add public SSO workspace info query Introduces the ssoWorkspace query to fetch public workspace info (id, name, image) for SSO login pages. Updates GraphQL type definitions with WorkspacePreview type and exposes ssoWorkspace query for unauthenticated access. * Update workspace.js * Enforce SSO login and refactor SSO config update Added enforcement of SSO login for users in workspaces with enforced SSO. Refactored SSO configuration update logic by introducing setSsoConfig method in WorkspaceModel and updating resolver to use it, ensuring only SSO config is modified. * add logs to the sso controller * Shorten refresh token expiry for enforced SSO users Refresh token lifetime is now 2 days instead of 30 for users in workspaces with enforced SSO. This change applies to both standard and SAML SSO flows to improve security by requiring more frequent re-authentication. * Create sso.test.ts * fixes for sso * integration tests * Bump version up to 1.2.33 * Update package.json * lint * fix tests * Add pluggable SAML state store with Redis and memory support Refactored SAML state management to support both Redis and in-memory stores via a new SamlStateStoreInterface. Added Redis-backed implementation for multi-instance deployments and a factory to select the store type based on the SAML_STORE_TYPE environment variable. Updated controller and router to use the new store abstraction, and extended environment and type definitions accordingly. * fix unti tests * fix integration tests --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 47936e2 commit c8b18bf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+5518
-25
lines changed

.env.sample

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,10 @@ AWS_S3_SECRET_ACCESS_KEY=
8686
AWS_S3_BUCKET_NAME=
8787
AWS_S3_BUCKET_BASE_URL=
8888
AWS_S3_BUCKET_ENDPOINT=
89+
90+
# SSO Service Provider Entity ID
91+
# Unique identifier for Hawk in SAML IdP configuration
92+
SSO_SP_ENTITY_ID=urn:hawk:tracker:saml
93+
94+
## SAML state store type (memory or redis, default: redis)
95+
SAML_STORE_TYPE=redis

.env.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,6 @@ AWS_S3_SECRET_ACCESS_KEY=
9797
AWS_S3_BUCKET_NAME=
9898
AWS_S3_BUCKET_BASE_URL=
9999
AWS_S3_BUCKET_ENDPOINT=
100+
101+
## SAML state store type (memory or redis, default: redis)
102+
SAML_STORE_TYPE=memory

.github/workflows/build-and-push-docker-image.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,19 @@ jobs:
4848
type=semver,pattern={{version}}
4949
type=semver,pattern={{major}}.{{minor}}
5050
51+
- name: Read Node.js version from .nvmrc
52+
id: node_version
53+
run: |
54+
NODE_VERSION=$(cat .nvmrc | tr -d 'v')
55+
echo "version=${NODE_VERSION}" >> $GITHUB_OUTPUT
56+
5157
- name: Build and push image
5258
uses: docker/build-push-action@v3
5359
with:
5460
context: .
5561
file: docker/Dockerfile.prod
62+
build-args: |
63+
NODE_VERSION=${{ steps.node_version.outputs.version }}
5664
tags: ${{ steps.meta.outputs.tags }}
5765
labels: ${{ steps.meta.outputs.labels }}
5866
push: ${{ github.ref == 'refs/heads/stage' || github.ref == 'refs/heads/prod' || startsWith(github.ref, 'refs/tags/v') }}

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v22.12.0
1+
v24.11.1

docker-compose.test.yml

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ services:
1111
- ./:/usr/src/app
1212
- /usr/src/app/node_modules
1313
- ./test/integration/api.env:/usr/src/app/.env
14+
- ./test/integration/keycloak:/keycloak:ro
1415
depends_on:
1516
- mongodb
1617
- rabbitmq
18+
- keycloak
1719
# - accounting
1820
stdin_open: true
1921
tty: true
@@ -32,10 +34,20 @@ services:
3234
condition: service_healthy
3335
api:
3436
condition: service_started
35-
command: dockerize -wait http://api:4000/.well-known/apollo/server-health -timeout 30s yarn jest --config=./test/integration/jest.config.js --runInBand test/integration
37+
keycloak:
38+
condition: service_healthy
39+
environment:
40+
- KEYCLOAK_URL=http://keycloak:8180
41+
entrypoint: ["/bin/bash", "-c"]
42+
command:
43+
- |
44+
dockerize -wait http://api:4000/.well-known/apollo/server-health -timeout 30s -wait http://keycloak:8180/health/ready -timeout 60s &&
45+
/keycloak/setup.sh &&
46+
yarn jest --config=./test/integration/jest.config.js --runInBand test/integration
3647
volumes:
3748
- ./:/usr/src/app
3849
- /usr/src/app/node_modules
50+
- ./test/integration/keycloak:/keycloak:ro
3951

4052
rabbitmq:
4153
image: rabbitmq:3-management
@@ -52,6 +64,29 @@ services:
5264
timeout: 3s
5365
retries: 5
5466

67+
keycloak:
68+
image: quay.io/keycloak/keycloak:23.0
69+
environment:
70+
- KEYCLOAK_ADMIN=admin
71+
- KEYCLOAK_ADMIN_PASSWORD=admin
72+
- KC_HTTP_PORT=8180
73+
- KC_HOSTNAME_STRICT=false
74+
- KC_HOSTNAME_STRICT_HTTPS=false
75+
- KC_HTTP_ENABLED=true
76+
- KC_HEALTH_ENABLED=true
77+
ports:
78+
- 8180:8180
79+
command:
80+
- start-dev
81+
volumes:
82+
- keycloak-test-data:/opt/keycloak/data
83+
- ./test/integration/keycloak:/opt/keycloak/config
84+
healthcheck:
85+
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8180;echo -e 'GET /health/ready HTTP/1.1\r\nhost: http://localhost\r\nConnection: close\r\n\r\n' >&3;if [ $? -eq 0 ]; then echo 'Healthcheck Successful';exit 0;else echo 'Healthcheck Failed';exit 1;fi;"]
86+
interval: 10s
87+
timeout: 5s
88+
retries: 10
89+
5590
# accounting:
5691
# image: codexteamuser/codex-accounting:prod
5792
# env_file:
@@ -61,3 +96,4 @@ services:
6196

6297
volumes:
6398
mongodata-test:
99+
keycloak-test-data:

docker/Dockerfile.dev

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
FROM node:22-alpine as builder
1+
ARG NODE_VERSION=24.11.1
2+
FROM node:${NODE_VERSION}-alpine as builder
23

34
WORKDIR /usr/src/app
45
RUN apk add --no-cache git gcc g++ python3 make musl-dev
@@ -7,11 +8,11 @@ COPY package.json yarn.lock ./
78

89
RUN yarn install
910

10-
FROM node:22-alpine
11+
FROM node:${NODE_VERSION}-alpine
1112

1213
WORKDIR /usr/src/app
1314

14-
RUN apk add --no-cache openssl
15+
RUN apk add --no-cache openssl bash curl
1516

1617
ENV DOCKERIZE_VERSION v0.6.1
1718
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \

docker/Dockerfile.prod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
FROM node:22-alpine as builder
1+
ARG NODE_VERSION=24.11.1
2+
FROM node:${NODE_VERSION}-alpine as builder
23

34
WORKDIR /usr/src/app
45
RUN apk add --no-cache git gcc g++ python3 make musl-dev
@@ -11,7 +12,7 @@ COPY . .
1112

1213
RUN yarn build
1314

14-
FROM node:22-alpine
15+
FROM node:${NODE_VERSION}-alpine
1516

1617
WORKDIR /usr/src/app
1718

docs/Keycloak.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Keycloak for Hawk SSO Development
2+
3+
This guide explains how to use Keycloak for testing Hawk's SSO implementation.
4+
5+
## Quick Start
6+
7+
### 1. Start Keycloak
8+
9+
From the project root:
10+
11+
```bash
12+
docker-compose up keycloak
13+
```
14+
15+
Keycloak will be available at: **http://localhost:8180**
16+
17+
### 2. Run Setup Script
18+
19+
The setup script will configure Keycloak with a test realm, SAML client, and test users.
20+
21+
**Option 1: Run from your host machine** (recommended):
22+
23+
```bash
24+
cd api/test/integration/keycloak
25+
KEYCLOAK_URL=http://localhost:8180 ./setup.sh
26+
```
27+
28+
**Option 2: Run from API container** (if you don't have curl on host):
29+
30+
```bash
31+
docker-compose exec -e KEYCLOAK_URL=http://keycloak:8180 api /keycloak/setup.sh
32+
```
33+
34+
**Note:** The setup script requires `curl` and `bash` to interact with Keycloak API. The Keycloak container doesn't have these tools, so we either run from host or from another container (like `api`).
35+
36+
### 3. Access Keycloak Admin Console
37+
38+
- URL: http://localhost:8180
39+
- Username: `admin`
40+
- Password: `admin`
41+
42+
## Configuration
43+
44+
### Realm
45+
46+
- **Name**: `hawk`
47+
- **SAML Endpoint**: http://localhost:8180/realms/hawk/protocol/saml
48+
49+
### SAML Client
50+
51+
- **Client ID / Entity ID**: `urn:hawk:tracker:saml`
52+
- This must match `SSO_SP_ENTITY_ID` environment variable in Hawk API
53+
- **Protocol**: SAML 2.0
54+
- **ACS URL**: http://localhost:4000/auth/sso/saml/{workspaceId}/acs
55+
- **Name ID Format**: email
56+
57+
### Environment Variables
58+
59+
Hawk API requires the following environment variable:
60+
61+
- **SSO_SP_ENTITY_ID**: `urn:hawk:tracker:saml`
62+
- Set in `docker-compose.yml` or `.env` file
63+
- This is the Service Provider Entity ID used to identify Hawk in SAML requests
64+
65+
### Test Users
66+
67+
| Username | Email | Password | Department | Title |
68+
|----------|-------|----------|------------|-------|
69+
| testuser | [email protected] | password123 | Engineering | Software Engineer |
70+
| alice | [email protected] | password123 | Product | Product Manager |
71+
| bob | [email protected] | password123 | Engineering | Senior Developer |
72+
73+
## Hawk SSO Configuration
74+
75+
To configure SSO in Hawk workspace settings:
76+
77+
### Get Configuration Automatically
78+
79+
**Option 1: Use the helper script** (recommended):
80+
81+
```bash
82+
cd api/test/integration/keycloak
83+
./get-config.sh
84+
```
85+
86+
This will output all required values that you can copy-paste into Hawk SSO settings.
87+
88+
**Option 2: Get values manually**:
89+
90+
### Required Fields
91+
92+
1. **IdP Entity ID**:
93+
```
94+
http://localhost:8180/realms/hawk
95+
```
96+
97+
2. **SSO URL**:
98+
```
99+
http://localhost:8180/realms/hawk/protocol/saml
100+
```
101+
102+
3. **X.509 Certificate**:
103+
104+
**Via command line**:
105+
```bash
106+
curl -s "http://localhost:8180/realms/hawk/protocol/saml/descriptor" | grep -oP '(?<=<ds:X509Certificate>)[^<]+' | head -1
107+
```
108+
109+
**Via Keycloak Admin Console**:
110+
- Go to Realm Settings → Keys
111+
- Find RS256 algorithm row
112+
- Click "Certificate" button
113+
- Copy the certificate (without BEGIN/END lines)
114+
- Paste into Hawk SSO settings
115+
116+
### Attribute Mapping
117+
118+
Configure these mappings in Hawk:
119+
120+
- **Email**: `email`
121+
- **Name**: `name` (full name - combines firstName and lastName from Keycloak)
122+
- **Department** (optional): `department`
123+
- **Title** (optional): `title`
124+
125+
### Name ID Format
126+
127+
Select: **Email address (urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress)**
128+
129+
## Testing SSO Flow
130+
131+
### Manual Test
132+
133+
1. Configure SSO in Hawk workspace settings with the values above
134+
2. Enable SSO for the workspace
135+
3. Navigate to: http://localhost:4000/auth/sso/saml/{workspaceId}
136+
4. You'll be redirected to Keycloak login page
137+
5. Login with any test user (e.g., `[email protected]` / `password123`)
138+
6. After successful authentication, you'll be redirected back to Hawk with tokens
139+
140+
### Automated Test
141+
142+
Run integration tests:
143+
144+
```bash
145+
cd api
146+
yarn test:integration
147+
```
148+
149+
## Troubleshooting
150+
151+
### Keycloak not starting
152+
153+
Check Docker logs:
154+
```bash
155+
docker-compose logs keycloak
156+
```
157+
158+
### Realm already exists
159+
160+
If you need to reset:
161+
```bash
162+
docker-compose down -v
163+
docker-compose up keycloak
164+
```
165+
166+
### Certificate issues
167+
168+
If SAML validation fails:
169+
1. Verify the certificate is copied correctly (no extra spaces/newlines)
170+
2. Ensure you copied the certificate content without BEGIN/END markers
171+
3. Check Keycloak logs for signature errors
172+
173+
### Get SAML Metadata
174+
175+
You can view the full SAML metadata descriptor at:
176+
```
177+
http://localhost:8180/realms/hawk/protocol/saml/descriptor
178+
```
179+
180+
This contains all technical details about the IdP configuration.
181+
182+
## Files
183+
184+
Files are located in `api/test/integration/keycloak/`:
185+
186+
- `import/hawk-realm.json` - Keycloak realm configuration
187+
- `setup.sh` - Automated setup script
188+
189+
## Advanced Configuration
190+
191+
### Custom Workspace ID
192+
193+
To test with a different workspace ID, update the ACS URL in the Keycloak Admin Console:
194+
195+
1. Go to Clients → hawk-sp
196+
2. Update `saml_assertion_consumer_url_post` attribute
197+
3. Save changes
198+
199+
### Additional Users
200+
201+
You can add more users through:
202+
- Keycloak Admin Console → Users → Add User
203+
- Or update `api/test/integration/keycloak/import/hawk-realm.json` and re-import
204+
205+
### Different Port
206+
207+
If you need to run Keycloak on a different port:
208+
209+
1. Update `KC_HTTP_PORT` in `docker-compose.yml`
210+
2. Update port mapping in `docker-compose.yml`
211+
3. Update all URLs in this README
212+
4. Update `api/test/integration/keycloak/import/hawk-realm.json` with new URLs

jest.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ module.exports = {
2424
* TypeScript support
2525
*/
2626
transform: {
27-
'^.+\\.tsx?$': 'ts-jest',
27+
'^.+\\.tsx?$': ['ts-jest', {
28+
tsconfig: 'test/tsconfig.json',
29+
}],
2830
},
2931

3032
/**

0 commit comments

Comments
 (0)