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 1 commit
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
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
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 @@ -24,6 +24,7 @@ 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})

Expand Down Expand Up @@ -165,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}`)
})
5 changes: 3 additions & 2 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 @@ -25,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 @@ -41,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
5 changes: 5 additions & 0 deletions src/routes/consent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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)

Expand Down Expand Up @@ -37,6 +38,7 @@ export const getConsent = (
// 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);
Expand All @@ -56,6 +58,7 @@ export const getConsent = (
})
// This will handle any error that happens when making HTTP calls to hydra
.catch(function (error:any) {
logger.error(error)
next(error);
});
};
Expand All @@ -81,6 +84,7 @@ export const postConsent = (
})
// This will handle any error that happens when making HTTP calls to hydra
.catch(function (error:any) {
logger.error(error)
next(error);
});
}
Expand Down Expand Up @@ -123,6 +127,7 @@ export const postConsent = (
})
// This will handle any error that happens when making HTTP calls to hydra
.catch(function (error:any) {
logger.error(error)
next(error);
});
};
40 changes: 18 additions & 22 deletions src/routes/hydra.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {NextFunction, Request, Response} from 'express'
import config from '../config'
import {PublicApi} from '@oryd/kratos-client'
import config, {logger} from '../config'
import {PublicApi, Session} from '@oryd/kratos-client'
import {AdminApi as HydraAdminApi, AcceptLoginRequest} from '@oryd/hydra-client'
import url from 'url';

Expand All @@ -19,13 +19,13 @@ export default (
const currentLocation = `${req.protocol}://${req.headers.host}${req.url}`;

const query = url.parse(req.url, true).query;
// TODO FIGURE OUT HOW TO DO LOGIN AND REGISTRATION PAGES WITHOUT COOKIE FOR KEEPING THE CHALLENGE

let challenge = query.login_challenge || req.cookies.login_challenge;
if (challenge != null && challenge != "undefined") {
console.log("Writing the login_challenge cookie from hydra: " + challenge);
res.cookie("login_challenge", challenge);
logger.debug("Writing the login_challenge cookie from hydra: " + challenge);
logger.debug("login_challenge", challenge);
} else {
console.log("challenge exists: "+challenge)
logger.debug("challenge exists: "+challenge)
}
// ToDo Need to check more specific here!
const kratos_session = req.cookies.ory_kratos_session
Expand All @@ -35,21 +35,20 @@ export default (
// 3. Initiate login flow with Kratos
// prompt=login forces a new login from kratos regardless of browser sessions - this is important because we are letting Hydra handle sessions
// redirect_to ensures that when we redirect back to this url, we will have both the initial hydra challenge and the kratos request id in query params
console.log(" --> no request, not authenticated. Redirecting to kratos");
logger.info("no request, not authenticated. Redirecting to kratos");
let return_to_address = encodeURI(currentLocation);
res.redirect(`${config.kratos.browser}/self-service/browser/flows/login?prompt=login&return_to=${return_to_address}`);
return
}
if (!challenge) {
console.log("No request but also no challenge, Invalid request!");
logger.error("No request, no challenge, Invalid request!");
next()
return
} else {
// 1. Parse Hydra challenge from query params
// The challenge is used to fetch information about the login request from ORY Hydra.
// Means we have just been redirected from Hydra, and are on the login page
// We must check the hydra session to see if we can skip login
console.log("Checking Hydra Sessions");
// 2. Call Hydra and check the session of this user

return hydraAdminEndpoint.getLoginRequest(challenge)
Expand All @@ -63,7 +62,7 @@ export default (
let acceptLoginRequest = new AcceptLoginRequest()

acceptLoginRequest.subject = String(body.subject)
Copy link
Member

Choose a reason for hiding this comment

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

Is string really required here?

Copy link
Author

Choose a reason for hiding this comment

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

body.subject is "string | undefined". acceptLoginRequest.subject is "string". So this is necessary, right?!
I could also do:
acceptLoginRequest.subject = (body.subject as string)

console.log("acceptLoginRequest "+acceptLoginRequest)
logger.info("acceptLoginRequest "+acceptLoginRequest)
return hydraAdminEndpoint.acceptLoginRequest(challenge, acceptLoginRequest
// All we need to do is to confirm that we indeed want to log in the user.

Expand All @@ -76,36 +75,33 @@ export default (
kratosPublicEndpoint
// We need to know who the user is for hydra
.whoami(req as { headers: { [name: string]: string } })
.then( ({ body, response }) => {
.then( ({body, response} ) => {
// User is authenticated, accept the LoginRequest and tell Hydra
let acceptLoginRequest = new AcceptLoginRequest()
let acceptLoginRequest : AcceptLoginRequest = new AcceptLoginRequest()
// We should probably use "id" here but the gitlab-scenario only works via "email"
//acceptLoginRequest.subject = body.identity.id
acceptLoginRequest.subject = body.identity.traits.email
acceptLoginRequest.subject = (body.identity.traits as { email: string }).email || "[email protected]"

return hydraAdminEndpoint.acceptLoginRequest(challenge, acceptLoginRequest
).then((hydraResponse: any) => {
// All we need to do now is to redirect the user back to hydra!
res.redirect(hydraResponse.body.redirectTo)
})
.catch((err:any) => {
// Something went wrong with validating the whoami answer
console.log(err)
next(err)
});
})
})
.catch((err:any) => {
// Something went wrong with the whoami call
console.log(err)
logger.error(err)
next(err)
});
} else {
console.log("Request and challenge are bot set but user is not authenticated. Unknown state!")
logger.error("Request and challenge are both set but user is not authenticated. Unknown state!")
res.status(400).send('Request and challenge are bot set but user is not authenticated. Please try again!');
next()
}
})
.catch((err:any) => {
// Something went wrong with validating hydra's challenge getting the LoginRequest
console.log(err)
logger.error(err)
next()
});
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/recovery.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 {CommonApi} from '@oryd/kratos-client'
import {IncomingMessage} from 'http'

Expand All @@ -11,7 +11,7 @@ export default (req: Request, res: Response, next: NextFunction) => {
// 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 recovery flow.')
logger.info('No request found in URL, initializing recovery flow.')
res.redirect(`${config.kratos.browser}/self-service/browser/flows/recovery`)
return
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/settings.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 {CommonApi} from '@oryd/kratos-client'

const commonApi = new CommonApi(config.kratos.admin)
Expand All @@ -9,7 +9,7 @@ const settingsHandler = (req: Request, res: Response, next: NextFunction) => {
// The request is used to identify the login and registraion request and
// return data like the csrf_token and so on.
if (!request) {
console.log('No request found in URL, initializing flow.')
logger.info('No request found in URL, initializing settings flow.')
res.redirect(`${config.kratos.browser}/self-service/browser/flows/settings`)
return
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/verification.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 {CommonApi} from '@oryd/kratos-client'
import {IncomingMessage} from 'http'

Expand All @@ -11,7 +11,7 @@ export default (req: Request, res: Response, next: NextFunction) => {
// 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 verify flow.')
logger.info('No request found in URL, initializing verify flow.')
res.redirect(`${config.kratos.browser}/self-service/browser/flows/verification/email`)
return
}
Expand Down