Skip to content

Commit

Permalink
feat: add required environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
Benehiko committed Sep 13, 2023
1 parent 0a1fb89 commit d79a0da
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 37 deletions.
46 changes: 39 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,50 @@ registration, account recovery, ... screens, please check out the

## Configuration

This application can be configured using two environment variables:
Below is a list of environment variables required by the Express.js service to
function properly.

- `KRATOS_PUBLIC_URL` (required): The URL where ORY Kratos's Public API is
located at. If this app and ORY Kratos are running in the same private
network, this should be the private network address (e.g.
In a local development run of the service using `npm run start`, some of these
values will be set by nodemon and is configured by the `nodemon.json` file.

When using this UI with an Ory Network project, you can use `ORY_SDK_URL`
instead of `KRATOS_PUBLIC_URL` and `HYDRA_ADMIN_URL`.

Ory Identities requires the following variables to be set:

- `ORY_SDK_URL` or `KRATOS_PUBLIC_URL` (required): The URL where ORY Kratos's
Public API is located at. If this app and ORY Kratos are running in the same
private network, this should be the private network address (e.g.
`kratos-public.svc.cluster.local`).
- `KRATOS_BROWSER_URL` (optional) The browser accessible URL where ORY Kratos's
public API is located, only needed if it differs from `KRATOS_PUBLIC_URL`

Ory OAuth2 requires more setup to get CSRF cookies on the `/consent` endpoint.

- `ORY_SDK_URL` or `HYDRA_ADMIN_URL` (optional): The URL where Ory Hydra's
Public API is located at. If this app and Ory Hydra are running in the same
private network, this should be the private network address (e.g.
`hydra-admin.svc.cluster.local`)
- `COOKIE_SECRET` (required): Required for signing cookies. Must be a string
with at least 8 alphanumerical characters.
- `CSRF_COOKIE_SECRET` (optional): Required for the Consent route to set a CSRF
cookie with a hashed value. The value must be a string with at least 8
alphanumerical characters.
- `ORY_ADMIN_API_TOKEN` (optional): When using with an Ory Network project, you
should add the `ORY_ADMIN_API_TOKEN` for OAuth2 Consent flows.
- `CSRF_COOKIE_NAME` (optional): By default the CSRF cookie will be set to
`ax-x-csrf-token`.
- `DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES` (optional) This environment
variables should only be used in local development when you do not have HTTPS
setup. This sets the CSRF cookies to `secure: false`, required for running
locally.

Getting TLS working:

- `TLS_CERT_PATH` (optional): Path to certificate file. Should be set up
together with `TLS_KEY_PATH` to enable HTTPS.
- `TLS_KEY_PATH` (optional): Path to key file Should be set up together with
`TLS_CERT_PATH` to enable HTTPS.
- `KRATOS_BROWSER_URL` (optional) The browser accessible URL where ORY Kratos's
public API is located, only needed if it differs from `KRATOS_PUBLIC_URL`

This is the easiest mode as it requires no additional set up. This app runs on
port `:4455` and ORY Kratos `KRATOS_PUBLIC_URL` URL.
Expand Down Expand Up @@ -54,7 +86,7 @@ recommended.
To run this app with dummy data and no real connection to ORY Kratos, use:

```shell script
$ NODE_ENV=stub npm start
NODE_ENV=stub npm start
```

### Test with ORY Kratos
Expand Down
8 changes: 7 additions & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"watch": ["src"],
"ext": "ts",
"exec": "ts-node ./src/index.ts"
"exec": "ts-node ./src/index.ts",
"env": {
"COOKIE_SECRET": "I_AM_VERY_SECRET",
"CSRF_COOKIE_SECRET": "I_AM_VERY_SECRET_TOO",
"DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES": "true",
"ORY_SDK_URL": "http://localhost:4000"
}
}
37 changes: 28 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const app = express()
const router = express.Router()

app.use(middlewareLogger)
app.use(cookieParser())
app.use(cookieParser(process.env.COOKIE_SECRET))
app.use(addFavicon(defaultConfig))
app.use(detectLanguage)
app.set("view engine", "hbs")
Expand Down Expand Up @@ -79,13 +79,32 @@ let listener = (proto: "http" | "https") => () => {
console.log(`Listening on ${proto}://0.0.0.0:${port}`)
}

if (process.env.TLS_CERT_PATH?.length && process.env.TLS_KEY_PATH?.length) {
const options = {
cert: fs.readFileSync(process.env.TLS_CERT_PATH),
key: fs.readFileSync(process.env.TLS_KEY_PATH),
}

https.createServer(options, app).listen(port, listener("https"))
// When using the Ory Admin API Token, we assume that this application is also
// handling OAuth2 Consent requests. In that case we need to ensure that the
// COOKIE_SECRET and CSRF_COOKIE_SECRET environment variables are set.
if (
(process.env.ORY_ADMIN_API_TOKEN &&
String(process.env.COOKIE_SECRET || "").length < 8) ||
String(process.env.CSRF_COOKIE_SECRET || "").length < 8
) {
console.error(
"Cannot start the server without the required environment variables!",
)
console.error(
"COOKIE_SECRET must be set and be at least 8 alphanumerical character `export COOKIE_SECRET=...`",
)
console.error(
"CSRF_COOKIE_SECRET must be set and be at least 8 alphanumerical character `export CSRF_COOKIE_SECRET=...`",
)
} else {
app.listen(port, listener("http"))
if (process.env.TLS_CERT_PATH?.length && process.env.TLS_KEY_PATH?.length) {
const options = {
cert: fs.readFileSync(process.env.TLS_CERT_PATH),
key: fs.readFileSync(process.env.TLS_KEY_PATH),
}

https.createServer(options, app).listen(port, listener("https"))
} else {
app.listen(port, listener("http"))
}
}
15 changes: 8 additions & 7 deletions src/pkg/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ const apiBaseIdentityUrl = process.env.KRATOS_ADMIN_URL || baseUrlInternal
export const apiBaseUrl =
process.env.KRATOS_BROWSER_URL || apiBaseFrontendUrlInternal

const hydraBaseOptions: any = {}

if (process.env.MOCK_TLS_TERMINATION) {
hydraBaseOptions.headers = { "X-Forwarded-Proto": "https" }
}

// Sets up the SDK
const sdk = {
basePath: apiBaseFrontendUrlInternal,
Expand All @@ -32,7 +26,14 @@ const sdk = {
oauth2: new OAuth2Api(
new Configuration({
basePath: apiBaseOauth2UrlInternal,
baseOptions: hydraBaseOptions,
...(process.env.ORY_ADMIN_API_TOKEN && {
accessToken: process.env.ORY_ADMIN_API_TOKEN,
}),
...(process.env.MOCK_TLS_TERMINATION && {
baseOptions: {
"X-Forwarded-Proto": "https",
},
}),
}),
),
identity: new IdentityApi(
Expand Down
45 changes: 32 additions & 13 deletions src/routes/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,43 @@ import {
} from "@ory/client"
import { UserConsentCard } from "@ory/elements-markup"
import bodyParser from "body-parser"
import { doubleCsrf } from "csrf-csrf"
import { doubleCsrf, DoubleCsrfCookieOptions } from "csrf-csrf"
import { Request, Response, NextFunction } from "express"

const cookieOptions: DoubleCsrfCookieOptions = {
sameSite: "lax",
signed: true,
// set secure cookies by default (recommended in production)
// can be disabled through DANGEROUSLY_DISABLE_SECURE_COOKIES=true env var
secure: true,
...(process.env.DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES && {
secure: false,
}),
}

const cookieName = process.env.CSRF_COOKIE_NAME || "ax-x-csrf-token"
const cookieSecret = process.env.CSRF_COOKIE_SECRET

// Sets up csrf protection
const {
generateToken, // Use this in your routes to provide a CSRF hash + token cookie and token.
invalidCsrfTokenError,
doubleCsrfProtection, // This is the default CSRF protection middleware.
} = doubleCsrf({
getSecret: () => "VERY_SECRET_VALUE", // A function that optionally takes the request and returns a secret
cookieName: "ax-x-csrf-token", // The name of the cookie to be used, recommend using Host prefix.
cookieOptions: {
sameSite: "lax", // Recommend you make this strict if posible
secure: true,
},
getSecret: () => cookieSecret || "", // A function that optionally takes the request and returns a secret
cookieName: cookieName, // The name of the cookie to be used, recommend using Host prefix.
cookieOptions,
ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected.
getTokenFromRequest: (req) => req.headers["x-csrf-token"], // A function that returns the token from the request
})

// Checks if OAuth2 consent is enabled
// This is used to determine if the consent route should be registered
// We need to check if the environment variables are set
const isOAuthCosentEnabled = () =>
(process.env.HYDRA_ADMIN_URL || process.env.ORY_SDK_URL) &&
process.env.CSRF_COOKIE_SECRET

// Error handling, validation error interception
const csrfErrorHandler = (
error: unknown,
Expand All @@ -37,7 +55,11 @@ const csrfErrorHandler = (
next: NextFunction,
) => {
if (error == invalidCsrfTokenError) {
next(new Error("csrf validation error"))
next(
new Error(
"A security violation was detected, please fill out the form again.",
),
)
} else {
next()
}
Expand Down Expand Up @@ -82,7 +104,6 @@ async function createOAuth2ConsentRequestSession(
// A simple express handler that shows the Hydra consent screen.
export const createConsentRoute: RouteCreator =
(createHelpers) => (req, res, next) => {
console.log("createConsentRoute")
res.locals.projectName = "An application requests access to your data!"

const { oauth2, identity } = createHelpers(req, res)
Expand All @@ -102,7 +123,6 @@ export const createConsentRoute: RouteCreator =
trustedClients = String(process.env.TRUSTED_CLIENT_IDS).split(",")
}

console.log("getOAuth2ConsentRequest", challenge)
// This section processes consent requests and either shows the consent UI or
// accepts the consent request right away if the user has given consent to this
// app before
Expand Down Expand Up @@ -267,8 +287,7 @@ export const registerConsentRoute: RouteRegistrator = function (
app,
createHelpers = defaultConfig,
) {
if (process.env.HYDRA_ADMIN_URL) {
console.log("found HYDRA_ADMIN_URL")
if (isOAuthCosentEnabled()) {
return app.get("/consent", createConsentRoute(createHelpers))
} else {
return register404Route
Expand All @@ -279,7 +298,7 @@ export const registerConsentPostRoute: RouteRegistrator = function (
app,
createHelpers = defaultConfig,
) {
if (process.env.HYDRA_ADMIN_URL) {
if (isOAuthCosentEnabled()) {
return app.post(
"/consent",
parseForm,
Expand Down

0 comments on commit d79a0da

Please sign in to comment.