Skip to content

Integrating Hydra and Kratos #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
edd2a47
Initial steps bringing hydra calls into the Kratos example ui
Jun 20, 2020
dd37af9
refactor: 127.0.01 instead of localhost,
k9ert Jun 29, 2020
9cd346d
refactor: undone package-naming
k9ert Jul 1, 2020
eae1b7d
refactor: renaming hydraauth to hydra
k9ert Jul 1, 2020
91e5436
refactor: removed home-route
k9ert Jul 1, 2020
c51b516
refactor:fix logging and other minor stuff
Jul 1, 2020
4596b92
refactor: migrate consent to hydra-sdk and tidy up
Jul 1, 2020
5a23b10
refactor: remove quickstart, tidy up
Jul 1, 2020
4e15ca4
refactor: remove pure-js-dependencies
Jul 1, 2020
10c2d60
feat: add csrf-protection for consent-endpoint
Jul 2, 2020
634ec3c
fix: gitlab login works only is subject:email
Jul 3, 2020
6abb329
feat: setup winston logging
Jul 7, 2020
af0488b
Merge branch 'master' into master
aeneasr Jul 7, 2020
db986b6
fixes gitlab-email issue
Jul 10, 2020
067cc28
forgot to add winston lib
Jul 10, 2020
1a6bbb7
Merge branch 'master' of github.com:k9ert/kratos-selfservice-ui-node
Jul 10, 2020
85e9b4d
Merge branch 'master' into master
k9ert Jul 10, 2020
52f6e05
Typo: Update package.json
k9ert Jul 10, 2020
8b60ab8
Update src/config.ts: Remove comment
k9ert Jul 10, 2020
0797dcb
Update src/index.ts: rename
k9ert Jul 10, 2020
c27d922
Update src/index.ts
k9ert Jul 10, 2020
478e35d
Update src/index.ts
k9ert Jul 10, 2020
e4bef33
Update src/index.ts
k9ert Jul 10, 2020
0f0581f
npm run format
Jul 10, 2020
9056a21
Update src/routes/auth.ts
k9ert Jul 10, 2020
987f750
Update views/registration.hbs
k9ert Jul 10, 2020
1b96209
refactoring authInfo
Jul 13, 2020
9db1e4b
Merge branch 'master' of github.com:k9ert/kratos-selfservice-ui-node
Jul 13, 2020
864404d
minor improvements and best practices
Jul 13, 2020
1efb08c
minor improvements and best practices
Jul 13, 2020
e3aff58
Update views/login.hbs: empty line
k9ert Jul 13, 2020
be226e2
Update src/routes/consent.ts
k9ert Jul 13, 2020
3c4fda7
Update src/routes/consent.ts
k9ert Jul 13, 2020
3bab29b
tidy-up and beautifying
Jul 13, 2020
1c9119a
refactor: clean up code base and resolve issues
aeneasr Jul 14, 2020
ce1b787
Merge pull request #1 from ory/k9ert
k9ert Jul 17, 2020
2331d92
proxy-fix and consent-endpointadjustment
Jul 17, 2020
026e2b7
Merge remote-tracking branch 'origin/master' into k9ert
aeneasr Jul 21, 2020
ec2b74f
u
aeneasr Jul 21, 2020
3c21e68
u
aeneasr Jul 21, 2020
daf8a7e
u
aeneasr Jul 21, 2020
662a5ae
Merge remote-tracking branch 'origin/master' into k9ert
aeneasr Jul 21, 2020
750f9f6
u
aeneasr Jul 21, 2020
cd8284b
Merge remote-tracking branch 'origin/master' into k9ert
aeneasr Jul 21, 2020
d06b655
u
aeneasr Jul 21, 2020
80915e2
u
aeneasr Jul 21, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@ This application can be configured using two environment variables:
GitHub pages this would be the path, e.g. `https://mywebsite.com/kratos-selfservice-ui-node/`.
**Must be absolute!**

If you want to also use hydra and connect an app via OAuth2, set these env-variables:
- `HYDRA_ADMIN_URL` should point to hydra's admin port including scheme (e.g. https://hydra.example.com:445)

If you want to test hydra without the use of kratos for user-management, rather have a look at the [hydra-login-consent-node][https://github.com/ory/hydra-login-consent-node].

### Network Setup

This application works in two set ups:

- Standalone with ORY Kratos
- Standalone with ORY Kratos (plus optionally ORY Hydra)
- With the ORY Oathkeeper Reverse Proxy

#### Standalone using cookies
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "kratos-selfservice-ui-node",
"version": "0.0.1",
"description": "A reference implementation of a selfservice UI for ORY Kratos in node.js",
"description": "A reference implementation of a selfservice UI for ORY Ory Kratos in node.js",
"main": "src/index.ts",
"scripts": {
"build": "tsc",
Expand All @@ -22,8 +22,10 @@
},
"homepage": "https://github.com/ory/kratos-selfservice-ui-node#readme",
"dependencies": {
"@oryd/hydra-client": "^1.4.10",
"@oryd/kratos-client": "0.0.0-next.a3fbd19e6f44",
"@types/cookie-parser": "^1.4.2",
"@types/csurf": "^1.9.36",
"@types/express": "^4.17.2",
"@types/express-handlebars": "0.0.33",
"@types/handlebars-helpers": "^0.5.2",
Expand All @@ -32,6 +34,7 @@
"@types/node": "^12.12.27",
"@types/node-fetch": "^2.5.4",
"cookie-parser": "^1.4.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-handlebars": "^3.1.0",
"express-jwt": "^6.0.0",
Expand Down
14 changes: 14 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import winston from `winston`

// Replace this with how you think logging should look like
export const logger = winston.createLogger({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
transports: [
new winston.transports.Console(),
//new winston.transports.File({ filename: 'combined.log' })
]
});

export const SECURITY_MODE_STANDALONE = 'cookie'
export const SECURITY_MODE_JWT = 'jwt'

Expand Down
25 changes: 23 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import express, { Request, NextFunction, Response } from 'express'
import handlebars from 'express-handlebars'
import request from 'request'
import { authHandler } from './routes/auth'
import hydraauth from './routes/hydra'
import { getConsent, postConsent } from './routes/consent'
import errorHandler from './routes/error'
import dashboard from './routes/dashboard'
import debug from './routes/debug'
import config, { SECURITY_MODE_JWT, SECURITY_MODE_STANDALONE } from './config'
import config, { SECURITY_MODE_JWT, SECURITY_MODE_STANDALONE, logger } from './config'
import jwks from 'jwks-rsa'
import jwt from 'express-jwt'
import {
Expand All @@ -20,6 +22,11 @@ import settingsHandler from './routes/settings'
import verifyHandler from './routes/verification'
import recoveryHandler from './routes/recovery'
import morgan from 'morgan'
import bodyParser from 'body-parser'
import csrf from 'csurf'
import winston from 'winston'

const csrfProtection = csrf({cookie: true})

const protectOathKeeper = jwt({
// Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint.
Expand Down Expand Up @@ -105,11 +112,23 @@ if (process.env.NODE_ENV === 'stub') {
res.render('settings', stubs.settings)
})
app.get('/error', (_: Request, res: Response) => res.render('error'))
app.get('/consent', (_: Request, res: Response) => {
const config = stubs.registration.methods.password.config
res.render('consent', {
csrfToken: 'no CSRF!',
challenge: "challenge",
requested_scope: ["scope1", "scope2"],
user: "response.subject",
client: "response.client",
});
})
} else {
app.get('/', protect, dashboard)
app.get('/dashboard', protect, dashboard)
app.get('/auth/registration', authHandler('registration'))
app.get('/auth/login', authHandler('login'))
app.get('/auth/hydra/login', hydraauth)
app.get('/consent', protect, csrfProtection, getConsent, errorHandler)
app.post('/consent', protect, bodyParser.urlencoded({ extended: true }), csrfProtection, postConsent)
app.get('/error', errorHandler)
app.get('/settings', protect, settingsHandler)
app.get('/verify', verifyHandler)
Expand Down Expand Up @@ -147,5 +166,7 @@ app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const port = Number(process.env.PORT) || 3000
app.listen(port, () => {
console.log(`Listening on http://0.0.0.0:${port}`)
logger.info(`Listening on http://0.0.0.0:${port}`)
console.log(`Security mode: ${config.securityMode}`)
logger.info(`Security mode: ${config.securityMode}`)
})
8 changes: 5 additions & 3 deletions src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {NextFunction, Request, Response} from 'express'
import config from '../config'
import config, {logger} from '../config'
import {sortFormFields} from '../translations'
import {
AdminApi,
Expand All @@ -8,6 +8,7 @@ import {
RegistrationRequest,
} from '@oryd/kratos-client'
import {IncomingMessage} from 'http'
import url from 'url';

// A simple express handler that shows the login / registration screen.
// Argument "type" can either be "login" or "registration" and will
Expand All @@ -24,7 +25,7 @@ export const authHandler = (type: 'login' | 'registration') => (
// The request is used to identify the login and registration request and
// return data like the csrf_token and so on.
if (!request) {
console.log('No request found in URL, initializing auth flow.')
logger.info('No request found in URL, initializing auth flow.')
res.redirect(`${config.kratos.browser}/self-service/browser/flows/${type}`)
return
}
Expand All @@ -40,6 +41,7 @@ export const authHandler = (type: 'login' | 'registration') => (
authRequest
.then(({body, response}) => {
if (response.statusCode == 404 || response.statusCode == 410 || response.statusCode == 403) {
logger.warn(`redirecting to /self-service/browser/flows/${type} due to statusCode ${response.statusCode}`)
res.redirect(
`${config.kratos.browser}/self-service/browser/flows/${type}`
)
Expand Down Expand Up @@ -78,7 +80,7 @@ export const authHandler = (type: 'login' | 'registration') => (
password:methodConfig("password"),
})
})
.catch(err => {
.catch((err) => {
console.error(err)
next(err)
})
Expand Down
133 changes: 133 additions & 0 deletions src/routes/consent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { NextFunction, Request, Response } from 'express'
import {AdminApi as HydraAdminApi, AcceptConsentRequest, RejectRequest} from '@oryd/hydra-client'
import url from 'url';
import {logger} from '../config'

const hydraAdminEndpoint = new HydraAdminApi(process.env.HYDRA_ADMIN_URL)

export const getConsent = (
req: Request,
res: Response,
next: NextFunction
) => {
// Parses the URL query
var query = url.parse(req.url, true).query;
// The challenge is used to fetch information about the consent request from ORY Hydra.
var challenge = query.consent_challenge;

hydraAdminEndpoint.getConsentRequest(String(challenge))
// This will be called if the HTTP request was successful
.then(function (response:any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any is bad practice for typing as it disables typing completely. I think typescript should be smart enough to auto-assert the type here?

Suggested change
.then(function (response:any) {
.then(response) => {

I am also a bit confused because I would expect this to be:

.then({ body, response }) => {

Which is a shorthand for:

.then((result) => {
  const body = result.body
  const response = result.response

// If a user has granted this application the requested scope, hydra will tell us to not show the UI.
if (response.skip) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is valid, this should probably be (see comment above):

Suggested change
if (response.skip) {
if (body.skip) {

// You can apply logic here, for example grant another scope, or do whatever...

// Now it's time to grant the consent request. You could also deny the request if something went terribly wrong
const acceptConsentRequest = new AcceptConsentRequest()
// We can grant all scopes that have been requested - hydra already checked for us that no additional scopes
// are requested accidentally.
acceptConsentRequest.grantScope = response.requested_scope
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works, see above.

Suggested change
acceptConsentRequest.grantScope = response.requested_scope
acceptConsentRequest.grantScope = body.requested_scope

// ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this.
acceptConsentRequest.grantAccessTokenAudience = response.requested_access_token_audience
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
acceptConsentRequest.grantAccessTokenAudience = response.requested_access_token_audience
acceptConsentRequest.grantAccessTokenAudience = body.requested_access_token_audience

// The session allows us to set session data for id and access tokens
// acceptConsentRequest.session = {
// This data will be available when introspectin+ the token. Try to avoid sensitive information here,
// unless you limit who can introspect tokens.
// access_token: { foo: 'bar' },

// This data will be available in the ID token.
// id_token: { baz: 'bar' },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indentation will be lost after running npm run format, you can prevent that with:

Suggested change
// This data will be available when introspectin+ the token. Try to avoid sensitive information here,
// unless you limit who can introspect tokens.
// access_token: { foo: 'bar' },
// This data will be available in the ID token.
// id_token: { baz: 'bar' },
// This data will be available when introspectin+ the token. Try to avoid sensitive information here,
// unless you limit who can introspect tokens.
// access_token: { foo: 'bar' },
// This data will be available in the ID token.
// id_token: { baz: 'bar' },

//}
logger.info('Accepting ConsentReuqest')
return hydraAdminEndpoint.acceptConsentRequest(String(challenge), acceptConsentRequest).then(function (response:any) {
// All we need to do now is to redirect the user back to hydra!
res.redirect(response.body.redirectTo);
});
}

// If consent can't be skipped we MUST show the consent UI.
res.render('consent', {
csrfToken: req.csrfToken(),
challenge: challenge,
// We have a bunch of data available from the response, check out the API docs to find what these values mean
// and what additional data you have available.
requested_scope: response.requested_scope,
user: response.subject,
client: response.client,
});
})
// This will handle any error that happens when making HTTP calls to hydra
.catch(function (error:any) {
logger.error(error)
next(error);
});
};

export const postConsent = (
req: Request,
res: Response,
next: NextFunction
) => {
// The challenge is now a hidden input field, so let's take it from the request body instead
var challenge = req.body.challenge;
// Let's see if the user decided to accept or reject the consent request..
if (req.body.submit != 'Allow access') {
// Looks like the consent request was denied by the user
const rejectConsentRequest = new RejectRequest()
rejectConsentRequest.error = 'access_denied'
rejectConsentRequest.errorDescription = 'The resource owner denied the request'

return hydraAdminEndpoint.rejectConsentRequest( challenge, rejectConsentRequest)
.then(function (response:any) {
// All we need to do now is to redirect the browser back to hydra!
res.redirect(response.body.redirectTo);
})
// This will handle any error that happens when making HTTP calls to hydra
.catch(function (error:any) {
logger.error(error)
next(error);
});
}

var grant_scope = req.body.grant_scope
if (!Array.isArray(grant_scope)) {
grant_scope = [grant_scope]
}

// Seems like the user authenticated! Let's tell hydra...
hydraAdminEndpoint.getConsentRequest(challenge)
// This will be called if the HTTP request was successful
.then(function (response:any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

Suggested change
.then(function (response:any) {
.then(({ response, body }) {

const acceptConsentRequest = new AcceptConsentRequest()
// We can grant all scopes that have been requested - hydra already checked for us that no additional scopes
// are requested accidentally.
acceptConsentRequest.grantScope = grant_scope
// ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this.
acceptConsentRequest.grantAccessTokenAudience = response.requested_access_token_audience
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
acceptConsentRequest.grantAccessTokenAudience = response.requested_access_token_audience
acceptConsentRequest.grantAccessTokenAudience = body.requested_access_token_audience

// This tells hydra to remember this consent request and allow the same client to request the same
// scopes from the same user, without showing the UI, in the future.
acceptConsentRequest.remember = Boolean(req.body.remember),
// When this "remember" sesion expires, in seconds. Set this to 0 so it will never expire.
acceptConsentRequest.rememberFor = 3600
// The session allows us to set session data for id and access tokens
// acceptConsentRequest.session = {
// This data will be available when introspecting the token. Try to avoid sensitive information here,
// unless you limit who can introspect tokens.
// access_token: { foo: 'bar' },

// This data will be available in the ID token.
// id_token: { baz: 'bar' },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// This data will be available when introspecting the token. Try to avoid sensitive information here,
// unless you limit who can introspect tokens.
// access_token: { foo: 'bar' },
// This data will be available in the ID token.
// id_token: { baz: 'bar' },
// This data will be available when introspecting the token. Try to avoid sensitive information here,
// unless you limit who can introspect tokens.
// access_token: { foo: 'bar' },
// This data will be available in the ID token.
// id_token: { baz: 'bar' },

//}

return hydraAdminEndpoint.acceptConsentRequest(challenge, acceptConsentRequest)
.then(function (response:any) {
// All we need to do now is to redirect the user back to hydra!
res.redirect(response.body.redirectTo);
})
})
// This will handle any error that happens when making HTTP calls to hydra
.catch(function (error:any) {
logger.error(error)
next(error);
});
};
2 changes: 1 addition & 1 deletion src/routes/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import jd from 'jwt-decode'

type UserRequest = Request & { user: any }

const authInfo = (req: UserRequest) => {
export const authInfo = (req: UserRequest) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a bit confusing when importing this in other routes. How about moving this to a new file auth.ts on src?

if (config.securityMode === 'JWT') {
const bearer = req.header('authorization')
if (bearer) {
Expand Down
Loading