diff --git a/docker/oidc/mock-oidc-provider/.gitignore b/docker/oidc/mock-oidc-provider/.gitignore new file mode 100644 index 0000000..d3d84cb --- /dev/null +++ b/docker/oidc/mock-oidc-provider/.gitignore @@ -0,0 +1,2 @@ +!proxy.js +!oidc-mock-provider.js diff --git a/docker/oidc/mock-oidc-provider/Dockerfile b/docker/oidc/mock-oidc-provider/Dockerfile new file mode 100644 index 0000000..79f64df --- /dev/null +++ b/docker/oidc/mock-oidc-provider/Dockerfile @@ -0,0 +1,16 @@ +FROM mongodb/mongodb-enterprise-server:latest +USER root +RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl +ARG NODE_VERSION=20.10.0 +ARG NODE_PACKAGE=node-v$NODE_VERSION-linux-arm64 +ARG NODE_HOME=/opt/$NODE_PACKAGE +ENV NODE_PATH $NODE_HOME/lib/node_modules +ENV PATH $NODE_HOME/bin:$PATH +RUN curl https://nodejs.org/dist/v$NODE_VERSION/$NODE_PACKAGE.tar.gz | tar -xzC /opt/ +RUN mkdir -p /tmp/mock-provider && cd /tmp/mock-provider && npm init -y && npm install @mongodb-js/oidc-mock-provider +COPY start-server.sh /start-server.sh +COPY oidc-mock-provider.js /tmp/mock-provider/oidc-mock-provider.js +COPY proxy.js /tmp/mock-provider/proxy.js +ENTRYPOINT ["/start-server.sh"] diff --git a/docker/oidc/mock-oidc-provider/README.md b/docker/oidc/mock-oidc-provider/README.md new file mode 100644 index 0000000..7824ae3 --- /dev/null +++ b/docker/oidc/mock-oidc-provider/README.md @@ -0,0 +1,20 @@ +### MongoDB enterprise with mock OIDC provider auth enabled + +```sh +docker-compose -f oidc/mock-oidc-provider/docker-compose.yaml up +``` + +#### How to connect + +```sh +mongosh \ + --host localhost \ + --port 27096 \ + --authenticationMechanism MONGODB-OIDC +``` + +Connection string: + +``` +mongodb://localhost:27096/?authMechanism=MONGODB-OIDC +``` diff --git a/docker/oidc/mock-oidc-provider/config.ts b/docker/oidc/mock-oidc-provider/config.ts new file mode 100644 index 0000000..a2afd14 --- /dev/null +++ b/docker/oidc/mock-oidc-provider/config.ts @@ -0,0 +1,21 @@ +import path from 'path'; + +import ConnectionString from 'mongodb-connection-string-url'; + +const port = '27017'; + +const connectionString = new ConnectionString(`mongodb://localhost:${port}`); +connectionString.searchParams.set('authMechanism', 'MONGODB-OIDC'); + +export default { + dockerCompose: { + projectName: path.basename(__dirname), + yamlPath: path.resolve(__dirname, 'docker-compose.yaml'), + }, + waitOn: [`tcp:${port}`], + connections: { + oidc: { + connectionString: connectionString.href, + }, + }, +}; diff --git a/docker/oidc/mock-oidc-provider/docker-compose.yaml b/docker/oidc/mock-oidc-provider/docker-compose.yaml new file mode 100644 index 0000000..6127e1b --- /dev/null +++ b/docker/oidc/mock-oidc-provider/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3' +services: + mongodb-server-with-mock-oidc-provider: + build: . + ports: + - '27096:27017' + - '29091:29091' + environment: + - OIDC_TOKEN_PAYLOAD_EXPIRES_IN + # comma-separated list + - OIDC_TOKEN_PAYLOAD_GROUPS + - OIDC_TOKEN_PAYLOAD_SUB + - OIDC_TOKEN_PAYLOAD_AUD diff --git a/docker/oidc/mock-oidc-provider/oidc-mock-provider.js b/docker/oidc/mock-oidc-provider/oidc-mock-provider.js new file mode 100644 index 0000000..043dd91 --- /dev/null +++ b/docker/oidc/mock-oidc-provider/oidc-mock-provider.js @@ -0,0 +1,60 @@ +const { OIDCMockProvider } = require('@mongodb-js/oidc-mock-provider'); + +const DEFAULT_TOKEN_PAYLOAD = { + expires_in: process.env.OIDC_TOKEN_PAYLOAD_EXPIRES_IN + ? Number(process.env.OIDC_TOKEN_PAYLOAD_EXPIRES_IN) + : 3600, + payload: { + // Define the user information stored inside the access tokens. + groups: process.env.OIDC_TOKEN_PAYLOAD_GROUPS + ? process.env.OIDC_TOKEN_PAYLOAD_GROUPS.split(',') + : ['testgroup'], + sub: process.env.OIDC_TOKEN_PAYLOAD_SUB || 'testuser', + aud: process.env.OIDC_TOKEN_PAYLOAD_AUD || 'resource-server-audience-value', + }, +}; + +(async () => { + const port = process.argv[2]; + const proxyPort = process.argv[3]; + const serverOidcConfig = { + get issuer() { + return provider.issuer; + }, + clientId: 'testServer', + requestScopes: ['mongodbGroups'], + authorizationClaim: 'groups', + audience: 'resource-server-audience-value', + authNamePrefix: 'dev', + }; + const provider = await OIDCMockProvider.create({ + port: Number(port), + getTokenPayload() { + return DEFAULT_TOKEN_PAYLOAD; + }, + overrideRequestHandler(_url, req, res) { + console.log('[OIDC PROVIDER] %s %s', req.method, req.url); + if (req.url === '/server-oidc-config') { + res.setHeader('content-type', 'application/json'); + res.write(JSON.stringify(serverOidcConfig)); + res.end(); + } + }, + }); + console.log('[OIDC PROVIDER] Listening on %s', provider.issuer); + // To make sure oidc-mock-provider can be used by the server, we need to make + // sure that it's listening on the localhost and the issuer returned by the + // various mock provider requests is matching for all parts of the OIDC flow. + // + // This is tricky in docker environment on macos where we can't run the + // container attached to the host network. We can't use docker hostnames + // either because then we will not be passing various http localhost checks in + // server or oidc-plugin. + // + // To work around that we set up a proxy (see `./proxy.js`) that actually + // listens on all interfaces (0.0.0.0), while mock provider is only listening + // to localhost. To make sure that all responses returned by mock provider are + // matching the proxy address that is exposed outside, we override instance + // issuer property after we start the mock provider. + provider.issuer = `http://localhost:${proxyPort}`; +})(); diff --git a/docker/oidc/mock-oidc-provider/proxy.js b/docker/oidc/mock-oidc-provider/proxy.js new file mode 100644 index 0000000..5784b6e --- /dev/null +++ b/docker/oidc/mock-oidc-provider/proxy.js @@ -0,0 +1,28 @@ +// see ./oidc-mock-provider.js for why we need this proxy +const http = require('http'); + +const from = process.argv[2]; +const to = process.argv[3]; + +http + .createServer(function (clientReq, clientRes) { + console.log('[OIDC PROVIDER PROXY] %s %s', clientReq.method, clientReq.url); + + const options = { + hostname: 'localhost', + port: to, + path: clientReq.url, + method: clientReq.method, + headers: clientReq.headers, + }; + + const proxy = http.request(options, function (res) { + clientRes.writeHead(res.statusCode, res.headers); + res.pipe(clientRes, { end: true }); + }); + + clientReq.pipe(proxy, { end: true }); + }) + .listen(from, '0.0.0.0', () => { + console.log(`[OIDC PROVIDER PROXY] Listening on http://0.0.0.0:${from}`); + }); diff --git a/docker/oidc/mock-oidc-provider/start-server.sh b/docker/oidc/mock-oidc-provider/start-server.sh new file mode 100755 index 0000000..5878624 --- /dev/null +++ b/docker/oidc/mock-oidc-provider/start-server.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e +OIDC_PROVIDER_PORT=29090 +OIDC_PROVIDER_PROXY_PORT=29091 +node /tmp/mock-provider/oidc-mock-provider.js $OIDC_PROVIDER_PORT $OIDC_PROVIDER_PROXY_PORT & +node /tmp/mock-provider/proxy.js $OIDC_PROVIDER_PROXY_PORT $OIDC_PROVIDER_PORT & +echo Waiting to make sure that oidc mock provider and proxy are running +until $(curl --output /dev/null --silent --head --fail http://localhost:$OIDC_PROVIDER_PROXY_PORT/.well-known/openid-configuration); do + sleep 0.3 +done +echo Starting server +OIDC_IDENTITY_PROVIDERS="[$(curl --fail http://localhost:29091/server-oidc-config)]" +# This is original mongodb/mongodb-enterprise-server entrypoint +python3 /usr/local/bin/docker-entrypoint.py \ + --setParameter authenticationMechanisms="SCRAM-SHA-256,MONGODB-OIDC" \ + --setParameter enableTestCommands="true" \ + --setParameter oidcIdentityProviders="$OIDC_IDENTITY_PROVIDERS" diff --git a/scripts/ps-all.sh b/scripts/ps-all.sh index 013fd4b..addb9b4 100644 --- a/scripts/ps-all.sh +++ b/scripts/ps-all.sh @@ -7,3 +7,4 @@ docker-compose -f docker/sharded/docker-compose.yaml ps docker-compose -f docker/ssh/docker-compose.yaml ps docker-compose -f docker/tls/docker-compose.yaml ps docker-compose -f docker/kerberos/docker-compose.yaml ps +docker-compose -f docker/oidc/mock-oidc-provider/docker-compose.yaml ps diff --git a/scripts/start-all.sh b/scripts/start-all.sh index dfdc9e4..d665945 100644 --- a/scripts/start-all.sh +++ b/scripts/start-all.sh @@ -7,3 +7,4 @@ docker-compose -f docker/sharded/docker-compose.yaml up -d --force-recreate docker-compose -f docker/ssh/docker-compose.yaml up -d --force-recreate docker-compose -f docker/tls/docker-compose.yaml up -d --force-recreate docker-compose -f docker/kerberos/docker-compose.yaml up -d --force-recreate +docker-compose -f docker/oidc/mock-oidc-provider/docker-compose.yaml up -d --force-recreate diff --git a/scripts/stop-all.sh b/scripts/stop-all.sh index 8876e53..7bbcc0b 100644 --- a/scripts/stop-all.sh +++ b/scripts/stop-all.sh @@ -7,3 +7,4 @@ docker-compose -f docker/sharded/docker-compose.yaml down --remove-orphans --vol docker-compose -f docker/ssh/docker-compose.yaml down --remove-orphans --volumes docker-compose -f docker/tls/docker-compose.yaml down --remove-orphans --volumes docker-compose -f docker/kerberos/docker-compose.yaml down --remove-orphans --volumes +docker-compose -f docker/oidc/mock-oidc-provider/docker-compose.yaml down --remove-orphans --volumes diff --git a/src/index.ts b/src/index.ts index 5069ef9..6f2cb52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import scram from '../docker/scram/config'; import sharded from '../docker/sharded/config'; import ssh from '../docker/ssh/config'; import tls from '../docker/tls/config'; +import oidc from '../docker/oidc/mock-oidc-provider/config'; const CONFIGS: Record = { community, @@ -22,6 +23,7 @@ const CONFIGS: Record = { sharded, ssh, tls, + oidc, }; export default function createTestEnvironments(