Skip to content

Commit

Permalink
feat: hydra integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas Burtey committed Jul 23, 2023
1 parent a24cac9 commit f59254b
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 138 deletions.
22 changes: 22 additions & 0 deletions dev/ory/hydra.yml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 8 additions & 1 deletion dev/ory/oathkeeper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ authenticators:
subject_from: identity.id
extra_from: identity.traits

oauth2_introspection:
enabled: true
config:
introspection_url: http://hydra:4445/admin/oauth2/introspect
token_from:
header: Oauth2-Token

anonymous:
enabled: true
config:
Expand Down Expand Up @@ -60,7 +67,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
Expand Down
6 changes: 6 additions & 0 deletions dev/ory/oathkeeper_rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@
preserve_query: true
subject_from: identity.id
extra_from: identity.traits
- handler: oauth2_introspection
config:
introspection_url: http://hydra:4445/admin/oauth2/introspect
token_from:
header: Oauth2-Token

- handler: bearer_token
config:
check_session_url: http://kratos:4433/sessions/whoami
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@ services:
fulcrum:
ports:
- "50001:50001"
hydra:
ports:
- "4444:4444" # Public port
- "4445:4445" # Admin port
40 changes: 40 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ services:
- otel-agent
- oathkeeper
- mailslurper
- hydra
# - consent
restart: on-failure:10
integration-deps:
image: busybox
Expand Down Expand Up @@ -402,3 +404,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
89 changes: 89 additions & 0 deletions docs/hydra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// TODO: PKCE flow (for alby like client, or mobile client)
// TODO: login flow with cookie
// TODO: add/use email instead of phone

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, you, as the end user, will login with 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')
```

to do a PKCE session, add `--token-endpoint-auth-method none`

## 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 as the user when you add the Header `Oauth2-Token: {token}`. (not that Bearer should not be present, unlike for the Authorization header. seems to a oathkeeper quirks)

### 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'

15 changes: 12 additions & 3 deletions src/app/authentication/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ import {
rewardFailedLoginAttemptPerLoginIdentifierLimits,
} from "./ratelimits"

type LoginWithPhoneTokenResult = {
authToken: SessionToken
id: UserId
}

export const loginWithPhoneToken = async ({
phone,
code,
Expand All @@ -41,7 +46,7 @@ export const loginWithPhoneToken = async ({
phone: PhoneNumber
code: PhoneCode
ip: IpAddress
}): Promise<SessionToken | ApplicationError> => {
}): Promise<LoginWithPhoneTokenResult | ApplicationError> => {
{
const limitOk = await checkFailedLoginAttemptPerIpLimits(ip)
if (limitOk instanceof Error) return limitOk
Expand Down Expand Up @@ -75,14 +80,18 @@ export const loginWithPhoneToken = async ({
const kratosResult = await authService.createIdentityWithSession({ phone })
if (kratosResult instanceof Error) return kratosResult

return kratosResult.sessionToken
return { authToken: kratosResult.sessionToken, id: kratosResult.kratosUserId }
}

if (userId instanceof Error) return userId

const kratosResult = await authService.loginToken({ phone })
if (kratosResult instanceof Error) return kratosResult
return kratosResult.sessionToken

// FIXME: kratosResult.kratosUserId will be null when using 2FA
const id = kratosResult.kratosUserId as UserId

return { authToken: kratosResult.sessionToken, id }
}

export const loginWithPhoneCookie = async ({
Expand Down
1 change: 1 addition & 0 deletions src/graphql/admin/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ scalar AuthToken
type AuthTokenPayload {
authToken: AuthToken
errors: [Error!]!
id: ID!
}

"""
Expand Down
1 change: 1 addition & 0 deletions src/graphql/main/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ scalar AuthToken
type AuthTokenPayload {
authToken: AuthToken
errors: [Error!]!
id: ID!
}

"""
Expand Down
10 changes: 6 additions & 4 deletions src/graphql/root/mutation/user-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
},
})

Expand Down
3 changes: 3 additions & 0 deletions src/graphql/types/payload/auth-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const AuthTokenPayload = GT.Object({
authToken: {
type: AuthToken,
},
id: {
type: GT.NonNull(GT.ID),
},
}),
})

Expand Down
Loading

0 comments on commit f59254b

Please sign in to comment.