ory-am/hydra

View on GitHub
test/e2e/oauth2-client/src/index.js

Summary

Maintainability
F
4 days
Test Coverage
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0

const express = require("express")
const session = require("express-session")
const uuid = require("node-uuid")
const oauth2 = require("simple-oauth2")
const fetch = require("node-fetch")
const ew = require("express-winston")
const winston = require("winston")
const { Issuer } = require("openid-client")
const { URLSearchParams } = require("url")
const bodyParser = require("body-parser")
const jwksClient = require("jwks-rsa")
const jwt = require("jsonwebtoken")

const app = express()

app.use(bodyParser.urlencoded({ extended: true }))

const blacklistedSid = []

const isStatusOk = (res) =>
  res.ok
    ? Promise.resolve(res)
    : Promise.reject(
        new Error(`Received unexpected status code ${res.statusCode}`),
      )

const config = {
  url: process.env.AUTHORIZATION_SERVER_URL || "http://127.0.0.1:5004/",
  public: process.env.PUBLIC_URL || "http://127.0.0.1:5004/",
  admin: process.env.ADMIN_URL || "http://127.0.0.1:5001/",
  port: parseInt(process.env.PORT) || 5003,
}

const redirect_uri = `http://127.0.0.1:${config.port}`

app.use(
  ew.logger({
    transports: [new winston.transports.Console()],
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple(),
    ),
  }),
)

app.use(
  session({
    secret: "804cd9c9-b447-4df0-b9f0-3126893d3a8e",
    resave: false,
    saveUninitialized: true,
    cookie: {
      secure: false,
      httpOnly: true,
    },
  }),
)

const nc = (req) =>
  Issuer.discover(config.public).then((issuer) => {
    // This is necessary when working with docker...
    issuer.metadata.token_endpoint = new URL(
      "/oauth2/token",
      config.public,
    ).toString()
    issuer.metadata.jwks_uri = new URL(
      "/.well-known/jwks.json",
      config.public,
    ).toString()
    issuer.metadata.revocation_endpoint = new URL(
      "/oauth2/revoke",
      config.public,
    ).toString()
    issuer.metadata.introspection_endpoint = new URL(
      "/oauth2/introspect",
      config.admin,
    ).toString()

    return Promise.resolve(
      new issuer.Client({
        ...issuer.metadata,
        ...req.session.oidc_credentials,
      }),
    )
  })

app.get("/oauth2/code", async (req, res) => {
  const credentials = {
    client: {
      id: req.query.client_id,
      secret: req.query.client_secret,
    },
    auth: {
      tokenHost: config.public,
      authorizeHost: config.url,
      tokenPath: "/oauth2/token",
      authorizePath: "/oauth2/auth",
    },
  }

  const state = uuid.v4()
  const scope = req.query.scope || ""

  req.session.credentials = credentials
  req.session.state = state
  req.session.scope = scope.split(" ")

  res.redirect(
    oauth2.create(credentials).authorizationCode.authorizeURL({
      redirect_uri: `${redirect_uri}/oauth2/callback`,
      scope,
      state,
    }),
  )
})

app.get("/oauth2/callback", async (req, res) => {
  if (req.query.error) {
    res.send(JSON.stringify(Object.assign({ result: "error" }, req.query)))
    return
  }

  if (req.query.state !== req.session.state) {
    res.send(JSON.stringify({ result: "error", error: "states mismatch" }))
    return
  }

  if (!req.query.code) {
    res.send(JSON.stringify({ result: "error", error: "no code given" }))
    return
  }

  oauth2
    .create(req.session.credentials)
    .authorizationCode.getToken({
      redirect_uri: `${redirect_uri}/oauth2/callback`,
      scope: req.session.scope,
      code: req.query.code,
    })
    .then((token) => {
      req.session.oauth2_flow = { token } // code returns {access_token} because why not...
      res.send({ result: "success", token })
    })
    .catch((err) => {
      if (err.data.payload) {
        res.send(JSON.stringify(err.data.payload))
        return
      }
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/oauth2/refresh", function (req, res) {
  oauth2
    .create(req.session.credentials)
    .accessToken.create(req.session.oauth2_flow.token)
    .refresh()
    .then((token) => {
      req.session.oauth2_flow = token // refresh returns {token:{access_token}} because why not...
      res.send({ result: "success", token: token.token })
    })
    .catch((err) => {
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/oauth2/revoke", (req, res) => {
  oauth2
    .create(req.session.credentials)
    .accessToken.create(req.session.oauth2_flow.token)
    .revoke(req.query.type || "access_token")
    .then(() => {
      res.sendStatus(201)
    })
    .catch((err) => {
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/oauth2/validate-jwt", (req, res) => {
  const client = jwksClient({
    jwksUri: new URL("/.well-known/jwks.json", config.public).toString(),
  })

  jwt.verify(
    req.session.oauth2_flow.token.access_token,
    (header, callback) => {
      client.getSigningKey(header.kid, function (err, key) {
        const signingKey = key.publicKey || key.rsaPublicKey
        callback(null, signingKey)
      })
    },
    (err, decoded) => {
      if (err) {
        console.error(err)
        res.send(400)
        return
      }

      res.send(decoded)
    },
  )
})

app.get("/oauth2/introspect/at", (req, res) => {
  const params = new URLSearchParams()
  params.append("token", req.session.oauth2_flow.token.access_token)

  fetch(new URL("/oauth2/introspect", config.admin).toString(), {
    method: "POST",
    body: params,
  })
    .then(isStatusOk)
    .then((res) => res.json())
    .then((body) => res.json({ result: "success", body }))
    .catch((err) => {
      console.error(err)
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/oauth2/introspect/rt", async (req, res) => {
  const params = new URLSearchParams()
  params.append("token", req.session.oauth2_flow.token.refresh_token)

  fetch(new URL("/oauth2/introspect", config.admin).toString(), {
    method: "POST",
    body: params,
  })
    .then(isStatusOk)
    .then((res) => res.json())
    .then((body) => res.json({ result: "success", body }))
    .catch((err) => {
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

// client credentials

app.get("/oauth2/cc", (req, res) => {
  const credentials = {
    client: {
      id: req.query.client_id,
      secret: req.query.client_secret,
    },
    auth: {
      tokenHost: config.public,
      tokenPath: "/oauth2/token",
    },
    options: {
      authorizationMethod: "header",
    },
  }

  oauth2
    .create(credentials)
    .clientCredentials.getToken({ scope: req.query.scope.split(" ") })
    .then((token) => {
      res.send({ result: "success", token })
    })
    .catch((err) => {
      if (err.data.payload) {
        res.send(JSON.stringify(err.data.payload))
        return
      }

      res.send(JSON.stringify({ error: err.toString() }))
    })
})

// openid

app.get("/openid/code", async (req, res) => {
  const credentials = {
    client_id: req.query.client_id,
    client_secret: req.query.client_secret,
  }

  const state = uuid.v4()
  const nonce = uuid.v4()
  const scope = req.query.scope || ""

  req.session.oidc_credentials = credentials
  req.session.state = state
  req.session.nonce = nonce
  req.session.scope = scope.split(" ")

  const client = await nc(req)
  const url = client.authorizationUrl({
    redirect_uri: `${redirect_uri}/openid/callback`,
    scope: scope,
    state: state,
    nonce: nonce,
    prompt: req.query.prompt,
  })
  res.redirect(url)
})

app.get("/openid/callback", async (req, res) => {
  if (req.query.error) {
    res.send(JSON.stringify(Object.assign({ result: "error" }, req.query)))
    return
  }

  if (req.query.state !== req.session.state) {
    res.send(JSON.stringify({ result: "error", error: "states mismatch" }))
    return
  }

  if (!req.query.code) {
    res.send(JSON.stringify({ result: "error", error: "no code given" }))
    return
  }

  const client = await nc(req)
  client
    .authorizationCallback(`${redirect_uri}/openid/callback`, req.query, {
      state: req.session.state,
      nonce: req.session.nonce,
      response_type: "code",
    })
    .then((ts) => {
      req.session.openid_token = ts
      req.session.openid_claims = ts.claims
      res.send({ result: "success", token: ts, claims: ts.claims })
    })
    .catch((err) => {
      console.error(err)
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/openid/userinfo", async (req, res) => {
  const client = await nc(req)
  client
    .userinfo(req.session.openid_token.access_token)
    .then((ui) => res.json(ui))
    .catch((err) => {
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/openid/revoke/at", async (req, res) => {
  const client = await nc(req)
  client
    .revoke(req.session.openid_token.access_token)
    .then(() => res.json({ result: "success" }))
    .catch((err) => {
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/openid/revoke/rt", async (req, res) => {
  const client = await nc(req)
  client
    .revoke(req.session.openid_token.refresh_token)
    .then(() => res.json({ result: "success" }))
    .catch((err) => {
      res.send(JSON.stringify({ error: err.toString() }))
    })
})

app.get("/openid/session/end", async (req, res) => {
  const client = await nc(req)
  const state = uuid.v4()

  if (req.query.simple) {
    res.redirect(new URL("/oauth2/sessions/logout", config.public).toString())
  } else {
    req.session.logout_state = state
    res.redirect(
      client.endSessionUrl({
        state,
        id_token_hint:
          req.query.id_token_hint || req.session.openid_token.id_token,
      }),
    )
  }
})

app.get("/openid/session/end/fc", async (req, res) => {
  if (req.session.openid_claims.sid !== req.query.sid) {
    res.sendStatus(400)
    return
  }

  if (req.session.openid_claims.iss !== req.query.iss) {
    res.sendStatus(400)
    return
  }

  setTimeout(() => {
    req.session.destroy(() => {
      res.send("ok")
    })
  }, 500)
})

app.post("/openid/session/end/bc", (req, res) => {
  const client = jwksClient({
    jwksUri: new URL("/.well-known/jwks.json", config.public).toString(),
    cache: false,
  })

  jwt.verify(
    req.body.logout_token,
    (header, callback) => {
      client.getSigningKey(header.kid, (err, key) => {
        if (err) {
          console.error(err)
          res.sendStatus(400)
          return
        }

        callback(null, key.publicKey || key.rsaPublicKey)
      })
    },
    (err, decoded) => {
      if (err) {
        console.error(err)
        res.sendStatus(400)
        return
      }

      if (decoded.nonce) {
        console.error("nonce is set but should not be", decoded.nonce)
        res.sendStatus(400)
        return
      }

      if (decoded.sid.length === 0) {
        console.error("sid should be set but is not", decoded.sid)
        res.sendStatus(400)
        return
      }

      if (decoded.iss.indexOf(config.url) === -1) {
        console.error("issuer is mismatching", decoded.iss, config.url)
        res.sendStatus(400)
        return
      }

      blacklistedSid.push(decoded.sid)
      res.send("ok")
    },
  )
})

app.get("/openid/session/check", async (req, res) => {
  const { openid_claims: { sid = "" } = {} } = req.session

  if (blacklistedSid.indexOf(sid) > -1) {
    req.session.destroy(() => {
      res.json({ has_session: false })
    })
    return
  }

  res.json({
    has_session:
      Boolean(req.session.oauth2_flow) ||
      (Boolean(req.session.openid_token) && Boolean(req.session.openid_claims)),
  })
})

app.get("/empty", (req, res) => {
  res.setHeader("Content-Type", "text/html")
  res.send(Buffer.from("<div>Nothing to see here.</div>"))
})

app.listen(config.port, function () {
  console.log(`Listening on port ${config.port}!`)
})