From cc16259e3e85501bd65d470c081e7cefdbe40df6 Mon Sep 17 00:00:00 2001 From: Nicolas Burtey Date: Sun, 4 Jun 2023 05:24:02 +0100 Subject: [PATCH] chore: poc hydra --- dev/ory/hydra.yml | 22 +++++ dev/ory/oathkeeper.yml | 8 +- dev/ory/oathkeeper_rules.yaml | 3 + docker-compose.override.yml | 4 + docker-compose.yml | 40 ++++++++ docs/hydra.md | 116 ++++++++++++++++++++++++ src/app/auth/login.ts | 10 +- src/graphql/admin/schema.graphql | 1 + src/graphql/main/schema.graphql | 1 + src/graphql/root/mutation/user-login.ts | 10 +- src/graphql/types/payload/auth-token.ts | 3 + 11 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 dev/ory/hydra.yml create mode 100644 docs/hydra.md diff --git a/dev/ory/hydra.yml b/dev/ory/hydra.yml new file mode 100644 index 00000000000..8d69cc1d243 --- /dev/null +++ b/dev/ory/hydra.yml @@ -0,0 +1,22 @@ +serve: + cookies: + same_site_mode: Lax + +urls: + self: + issuer: http://127.0.0.1:4444 + consent: http://127.0.0.1:3000/consent + login: http://127.0.0.1:3000/login + logout: http://127.0.0.1:3000/logout + +secrets: + system: + - youReallyNeedToChangeThis + +oidc: + subject_identifiers: + supported_types: + - pairwise + - public + pairwise: + salt: youReallyNeedToChangeThis diff --git a/dev/ory/oathkeeper.yml b/dev/ory/oathkeeper.yml index 27510f19ce5..5f30144fcc6 100644 --- a/dev/ory/oathkeeper.yml +++ b/dev/ory/oathkeeper.yml @@ -19,6 +19,7 @@ authenticators: jwks_urls: - https://firebaseappcheck.googleapis.com/v1beta/jwks - file:///home/ory/jwks.json # ONLY FOR DEV, DO NOT USE IN PRODUCTION + bearer_token: enabled: true config: @@ -27,6 +28,11 @@ authenticators: subject_from: identity.id extra_from: identity.traits + oauth2_introspection: + enabled: true + config: + introspection_url: http://hydra:4445/admin/oauth2/introspect + anonymous: enabled: true config: @@ -54,7 +60,7 @@ mutators: config: jwks_url: file:///home/ory/jwks.json issuer_url: "galoy.io" - claims: '{"sub": "{{ print .Subject }}" }' + claims: '{"sub": "{{ print .Subject }}", card: "{{ print .Ext.card }}" }' noop: enabled: true diff --git a/dev/ory/oathkeeper_rules.yaml b/dev/ory/oathkeeper_rules.yaml index 413c8fc80f8..79d30bc469c 100644 --- a/dev/ory/oathkeeper_rules.yaml +++ b/dev/ory/oathkeeper_rules.yaml @@ -49,6 +49,9 @@ preserve_query: true subject_from: identity.id extra_from: identity.traits + - handler: oauth2_introspection + config: + introspection_url: http://hydra:4445/admin/oauth2/introspect - handler: bearer_token config: check_session_url: http://kratos:4433/sessions/whoami diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 85cc2966aae..62b4d303938 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -87,3 +87,7 @@ services: fulcrum: ports: - "50001:50001" + hydra: + ports: + - "4444:4444" # Public port + - "4445:4445" # Admin port diff --git a/docker-compose.yml b/docker-compose.yml index 60fb68629ac..dd1ff9c6550 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,8 @@ services: - otel-agent - oathkeeper - mailslurper + - hydra + # - consent restart: on-failure:10 integration-deps: image: busybox @@ -371,3 +373,41 @@ services: - SSL_CERTFILE=/tls.cert - SSL_KEYFILE=/tls.key command: ["Fulcrum", "/fulcrum.conf"] + hydra: + image: oryd/hydra:v2.1.2 + command: serve -c /home/ory/hydra.yml all --dev + volumes: + - type: bind + source: dev/ory + target: /home/ory + environment: + - DSN=postgres://hydra:secret@postgresdhydra:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + restart: unless-stopped + depends_on: + - hydra-migrate + - postgresdhydra + hydra-migrate: + image: oryd/hydra:v2.1.2 + environment: + - DSN=postgres://hydra:secret@postgresdhydra:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + command: migrate -c /home/ory/hydra.yml sql -e --yes + volumes: + - type: bind + source: dev/ory + target: /home/ory + restart: on-failure + depends_on: + - postgresdhydra + # consent: + # environment: + # - HYDRA_ADMIN_URL=http://hydra:4445 + # image: oryd/hydra-login-consent-node:v2.1.2 + # ports: + # - "3000:3000" + # restart: unless-stopped + postgresdhydra: + image: postgres:14.1 + environment: + - POSTGRES_USER=hydra + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=hydra \ No newline at end of file diff --git a/docs/hydra.md b/docs/hydra.md new file mode 100644 index 00000000000..84b83c7b0fb --- /dev/null +++ b/docs/hydra.md @@ -0,0 +1,116 @@ + +Make sure you have `hydra` command line installed + +```sh +brew install ory-hydra +``` + +# run the experiment: + +Follow the instructions below + + +On console 1: + +launch the hydra login consent node, which will provide the authentication (interactive with kratos API) and consent page. + +```sh +hydra-login-consent-node % yarn start +``` + +On console 2: +```sh +galoy % make start-deps +``` + +On console 3: +Follow the instructions below + + +## create a OAuth2 client + +Think of the client as the service that need to get the delegation access + +If you use concourse currently, you'll login with Google Workspace. + +When you login through concourse you'll use google workspace. The client is concourse in this example. + +For the galoy stack, some examples of clients could be Alby, a boltcard service, a nostr wallet connect service, an accountant that access the wallet in read only. + + +```sh +code_client=$(hydra create client \ + --endpoint http://127.0.0.1:4445 \ + --grant-type authorization_code,refresh_token \ + --response-type code,id_token \ + --format json \ + --scope openid --scope offline \ + --redirect-uri http://127.0.0.1:5555/callback +) + +code_client_id=$(echo $code_client | jq -r '.client_id') +code_client_secret=$(echo $code_client | jq -r '.client_secret') +``` + +## Initiate the request + +this simulate the front end client. +would be mobile app for adding a boltcard + +```sh +hydra perform authorization-code \ + --client-id $code_client_id \ + --client-secret $code_client_secret \ + --endpoint http://127.0.0.1:4444/ \ + --port 5555 \ + --scope openid --scope offline +``` + +do the login and consent + +copy the Access token to the mobile app. + +you are now connect through Hydra. + + +### limitations + +as both settings in Oathkeeper: + +``` + - handler: oauth2_introspection + config: + introspection_url: http://hydra:4445/admin/oauth2/introspect +``` + +and + +``` + - handler: bearer_token + config: + check_session_url: http://kratos:4433/sessions/whoami + preserve_path: true + preserve_query: true + subject_from: identity.id + extra_from: identity.traits +``` + +rely on Authorization header, currency if hydra fails for the oauth2_introspection, Oathkeeper return an Unauthorized response + +so the bearer_token is not be tested. + +To mitigate this issue, either: +- Hydra token should have a different header (not Authorization) than Kratos token +- All requests should come through Hydra + +### debug + +hydra introspect token \ + --format json-pretty \ + --endpoint http://127.0.0.1:4445/ \ + TOKEN +# OR +curl -X POST http://localhost:4445/admin/oauth2/introspect -d token=ory_at_TOKEN + +curl -I -X POST http://localhost:4456/decisions/graphql -H 'Authorization: Bearer ory_at_TOKEN' + diff --git a/src/app/auth/login.ts b/src/app/auth/login.ts index 50ba8d790b6..1970559d4f6 100644 --- a/src/app/auth/login.ts +++ b/src/app/auth/login.ts @@ -45,6 +45,11 @@ import { sendOathkeeperRequest } from "@services/oathkeeper" import jwksRsa from "jwks-rsa" import jsonwebtoken from "jsonwebtoken" +type LoginWithPhoneTokenResult = { + authToken: SessionToken + id: UserId +} + export const loginWithPhoneToken = async ({ phone, code, @@ -53,7 +58,7 @@ export const loginWithPhoneToken = async ({ phone: PhoneNumber code: PhoneCode ip: IpAddress -}): Promise => { +}): Promise => { { const limitOk = await checkFailedLoginAttemptPerIpLimits(ip) if (limitOk instanceof Error) return limitOk @@ -87,7 +92,8 @@ export const loginWithPhoneToken = async ({ } else if (kratosResult instanceof Error) { return kratosResult } - return kratosResult.sessionToken + + return { authToken: kratosResult.sessionToken, id: kratosResult.kratosUserId } } export const loginWithPhoneCookie = async ({ diff --git a/src/graphql/admin/schema.graphql b/src/graphql/admin/schema.graphql index d89e2da5965..116e7686774 100644 --- a/src/graphql/admin/schema.graphql +++ b/src/graphql/admin/schema.graphql @@ -57,6 +57,7 @@ scalar AuthToken type AuthTokenPayload { authToken: AuthToken errors: [Error!]! + id: ID! } """ diff --git a/src/graphql/main/schema.graphql b/src/graphql/main/schema.graphql index afc2518b661..122f2eabf60 100644 --- a/src/graphql/main/schema.graphql +++ b/src/graphql/main/schema.graphql @@ -97,6 +97,7 @@ scalar AuthToken type AuthTokenPayload { authToken: AuthToken errors: [Error!]! + id: ID! } """ diff --git a/src/graphql/root/mutation/user-login.ts b/src/graphql/root/mutation/user-login.ts index 8b05e4f5f0c..93fc555c80e 100644 --- a/src/graphql/root/mutation/user-login.ts +++ b/src/graphql/root/mutation/user-login.ts @@ -46,17 +46,19 @@ const UserLoginMutation = GT.Field<{ return { errors: [{ message: "ip is undefined" }] } } - const authToken = await Auth.loginWithPhoneToken({ + const res = await Auth.loginWithPhoneToken({ phone, code, ip, }) - if (authToken instanceof Error) { - return { errors: [mapAndParseErrorForGqlResponse(authToken)] } + if (res instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(res)] } } - return { errors: [], authToken } + const { authToken, id } = res + + return { errors: [], authToken, id } }, }) diff --git a/src/graphql/types/payload/auth-token.ts b/src/graphql/types/payload/auth-token.ts index f24ca3a4f54..0943e62cd84 100644 --- a/src/graphql/types/payload/auth-token.ts +++ b/src/graphql/types/payload/auth-token.ts @@ -12,6 +12,9 @@ const AuthTokenPayload = GT.Object({ authToken: { type: AuthToken, }, + id: { + type: GT.NonNull(GT.ID), + }, }), })