Repository: honojs/middleware
Pull request #980: feat(oidc-auth) Add initOidcAuthMiddleware and avoid mutating environment variables
Author: login: rajsite
Created: 2025-02-24T06:36:36Z | Merged: 2025-03-16T09:57:44Z

Resolves #979 and resolves #978 

### The author should do the following, if applicable

- [x] Add tests
- [x] Run tests
- [x] `yarn changeset` at the top of this repo and push the changeset
- [x] Follow [the contribution guide](https://github.com/honojs/middleware?tab=readme-ov-file#how-to-contribute)


Linked issue #979: oidc-auth mutates global environment variables
### Which middleware is the feature for?

@hono/oidc-auth

### What is the feature you are proposing?

The middleware mutates the global process environment variables. This seems unintentional / unnecessary as the middleware saves its configuration to the context after configuration. 

Diff:
diff --git a/.changeset/cold-ties-sin.md b/.changeset/cold-ties-sin.md
new file mode 100644
index 000000000..123764dc5
--- /dev/null
+++ b/.changeset/cold-ties-sin.md
@@ -0,0 +1,5 @@
+---
+'@hono/oidc-auth': minor
+---
+
+Add initOidcAuthMiddleware() and avoid mutating environment variables
diff --git a/packages/oidc-auth/README.md b/packages/oidc-auth/README.md
index 369cf4892..10158dd59 100644
--- a/packages/oidc-auth/README.md
+++ b/packages/oidc-auth/README.md
@@ -41,9 +41,9 @@ npm i hono @hono/oidc-auth
 
 ## Configuration
 
-The middleware requires the following environment variables to be set:
+The middleware requires the following variables to be set as either environment variables or by calling `initOidcAuthMiddleware`:
 
-| Environment Variable       | Description                                                                                                                                                       | Default Value                          |
+| Variable                   | Description                                                                                                                                                       | Default Value                          |
 | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
 | OIDC_AUTH_SECRET           | The secret key used for signing the session JWT. It is used to verify the JWT in the cookie and prevent tampering. (Must be at least 32 characters long)          | None, must be provided                 |
 | OIDC_AUTH_REFRESH_INTERVAL | The interval (in seconds) at which the session should be implicitly refreshed.                                                                                    | 15 \* 60 (15 minutes)                  |
@@ -140,6 +140,26 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(initOidcAuthMiddleware(config));
+```
+
+Or to leverage context, use the [`Context access inside Middleware arguments`](https://hono.dev/docs/guides/middleware#context-access-inside-middleware-arguments) pattern.
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  const config = {
+    // Create config using context
+  }
+  const middleware = initOidcAuthMiddleware(config)
+  return middleware(c, next)
+})
+```
+
 ## Author
 
 Yoshio HANAWA <https://github.com/hnw>
diff --git a/packages/oidc-auth/src/index.test.ts b/packages/oidc-auth/src/index.test.ts
index 3eef1b54d..4498b9d82 100644
--- a/packages/oidc-auth/src/index.test.ts
+++ b/packages/oidc-auth/src/index.test.ts
@@ -1,5 +1,6 @@
 import { Hono } from 'hono'
 import jwt from 'jsonwebtoken'
+import type * as oauth2 from 'oauth4webapi'
 import crypto from 'node:crypto'
 
 const MOCK_ISSUER = 'https://accounts.google.com'
@@ -156,7 +157,7 @@ vi.mock(import('oauth4webapi'), async (importOriginal) => {
   }
 })
 
-const { oidcAuthMiddleware, getAuth, processOAuthCallback, revokeSession } = await import('../src')
+const { oidcAuthMiddleware, getAuth, processOAuthCallback, revokeSession, initOidcAuthMiddleware, getClient } = await import('../src')
 
 const app = new Hono()
 app.get('/logout', async (c) => {
@@ -482,6 +483,7 @@ describe('processOAuthCallback()', () => {
   })
   test('Should respond with custom cookie name', async () => {
     const MOCK_COOKIE_NAME = (process.env.OIDC_COOKIE_NAME = 'custom-auth-cookie')
+    const defaultOidcAuthCookiePath = '/'
     const req = new Request(`${MOCK_REDIRECT_URI}?code=1234&state=${MOCK_STATE}`, {
       method: 'GET',
       headers: {
@@ -493,7 +495,7 @@ describe('processOAuthCallback()', () => {
     expect(res.status).toBe(302)
     expect(res.headers.get('set-cookie')).toMatch(
       new RegExp(
-        `${MOCK_COOKIE_NAME}=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`
+        `${MOCK_COOKIE_NAME}=[^;]+; Path=${defaultOidcAuthCookiePath}; HttpOnly; Secure`
       )
     )
   })
@@ -558,3 +560,39 @@ describe('RevokeSession()', () => {
     )
   })
 })
+describe('initOidcAuthMiddleware()', () => {
+  test('Should error if not called first in context', async () => {
+    const app = new Hono()
+    app.use('/*', oidcAuthMiddleware())
+    app.use(initOidcAuthMiddleware({}))
+    const req = new Request('http://localhost/', {
+      method: 'GET',
+      headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
+    })
+    const res = await app.request(req, {}, {})
+    expect(res).not.toBeNull()
+    expect(res.status).toBe(500)
+  })
+  test('Should prefer programmatically configured varaiables', async () => {
+    let client: oauth2.Client | undefined
+    const CUSTOM_OIDC_CLIENT_ID = 'custom-client-id'
+    const CUSTOM_OIDC_CLIENT_SECRET = 'custom-client-secret'
+    const app = new Hono()
+    app.use(initOidcAuthMiddleware({
+      OIDC_CLIENT_ID: CUSTOM_OIDC_CLIENT_ID,
+      OIDC_CLIENT_SECRET: CUSTOM_OIDC_CLIENT_SECRET
+    }))
+    app.use(async (c) => {
+      client = getClient(c)
+      return c.text('finished')
+    })
+    const req = new Request('http://localhost/', {
+      method: 'GET'
+    })
+    const res = await app.request(req, {}, {})
+    expect(res).not.toBeNull()
+    expect(res.status).toBe(200)
+    expect(client?.client_id).toBe(CUSTOM_OIDC_CLIENT_ID)
+    expect(client?.client_secret).toBe(CUSTOM_OIDC_CLIENT_SECRET)
+  })
+})
diff --git a/packages/oidc-auth/src/index.ts b/packages/oidc-auth/src/index.ts
index 8b1c714c1..601c652d5 100644
--- a/packages/oidc-auth/src/index.ts
+++ b/packages/oidc-auth/src/index.ts
@@ -46,7 +46,7 @@ export type OidcAuth = {
   ssnexp: number // session expiration time; if it's expired, revoke session and redirect to IdP
 } & OidcAuthClaims
 
-type OidcAuthEnv = {
+export type OidcAuthEnv = {
   OIDC_AUTH_SECRET: string
   OIDC_AUTH_REFRESH_INTERVAL?: string
   OIDC_AUTH_EXPIRES?: string
@@ -60,47 +60,84 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC middleware or functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const initOidcAuthMiddleware = (config: Partial<OidcAuthEnv>) => {
+  return createMiddleware(async (c, next) => {
+    setOidcAuthEnv(c, config)
+    await next()
+  })
+}
+
+/**
+ * Configure the OIDC variables.
+ */
+const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
+  const currentOidcAuthEnv = c.get('oidcAuthEnv')
+  if (currentOidcAuthEnv !== undefined) {
+    throw new HTTPException(500, { message: 'OIDC Auth env is already configured' })
+  }
+  const ev = env<Readonly<OidcAuthEnv>>(c)
+  const oidcAuthEnv = {
+    OIDC_AUTH_SECRET: config?.OIDC_AUTH_SECRET ?? ev.OIDC_AUTH_SECRET,
+    OIDC_AUTH_REFRESH_INTERVAL: config?.OIDC_AUTH_REFRESH_INTERVAL ?? ev.OIDC_AUTH_REFRESH_INTERVAL,
+    OIDC_AUTH_EXPIRES: config?.OIDC_AUTH_EXPIRES ?? ev.OIDC_AUTH_EXPIRES,
+    OIDC_ISSUER: config?.OIDC_ISSUER ?? ev.OIDC_ISSUER,
+    OIDC_CLIENT_ID: config?.OIDC_CLIENT_ID ?? ev.OIDC_CLIENT_ID,
+    OIDC_CLIENT_SECRET: config?.OIDC_CLIENT_SECRET ?? ev.OIDC_CLIENT_SECRET,
+    OIDC_REDIRECT_URI: config?.OIDC_REDIRECT_URI ?? ev.OIDC_REDIRECT_URI,
+    OIDC_SCOPES: config?.OIDC_SCOPES ?? ev.OIDC_SCOPES,
+    OIDC_COOKIE_PATH: config?.OIDC_COOKIE_PATH ?? ev.OIDC_COOKIE_PATH,
+    OIDC_COOKIE_NAME: config?.OIDC_COOKIE_NAME ?? ev.OIDC_COOKIE_NAME,
+    OIDC_COOKIE_DOMAIN: config?.OIDC_COOKIE_DOMAIN ?? ev.OIDC_COOKIE_DOMAIN,
+  }
+  if (oidcAuthEnv.OIDC_AUTH_SECRET === undefined) {
+    throw new HTTPException(500, { message: 'Session secret is not provided' })
+  }
+  if (oidcAuthEnv.OIDC_AUTH_SECRET.length < 32) {
+    throw new HTTPException(500, {
+      message: 'Session secrets must be at least 32 characters long',
+    })
+  }
+  if (oidcAuthEnv.OIDC_ISSUER === undefined) {
+    throw new HTTPException(500, { message: 'OIDC issuer is not provided' })
+  }
+  if (oidcAuthEnv.OIDC_CLIENT_ID === undefined) {
+    throw new HTTPException(500, { message: 'OIDC client ID is not provided' })
+  }
+  if (oidcAuthEnv.OIDC_CLIENT_SECRET === undefined) {
+    throw new HTTPException(500, { message: 'OIDC client secret is not provided' })
+  }
+  oidcAuthEnv.OIDC_REDIRECT_URI = oidcAuthEnv.OIDC_REDIRECT_URI ?? defaultOidcRedirectUri
+  if (!oidcAuthEnv.OIDC_REDIRECT_URI.startsWith('/')) {
+    try {
+      new URL(oidcAuthEnv.OIDC_REDIRECT_URI)
+    } catch (e) {
+      throw new HTTPException(500, {
+        message: 'The OIDC redirect URI is invalid. It must be a full URL or an absolute path',
+      })
+    }
+  }
+  oidcAuthEnv.OIDC_COOKIE_PATH = oidcAuthEnv.OIDC_COOKIE_PATH ?? defaultOidcAuthCookiePath
+  oidcAuthEnv.OIDC_COOKIE_NAME = oidcAuthEnv.OIDC_COOKIE_NAME ?? defaultOidcAuthCookieName
+  oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL =
+    oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL ?? `${defaultRefreshInterval}`
+  oidcAuthEnv.OIDC_AUTH_EXPIRES = oidcAuthEnv.OIDC_AUTH_EXPIRES ?? `${defaultExpirationInterval}`
+  oidcAuthEnv.OIDC_SCOPES = oidcAuthEnv.OIDC_SCOPES ?? ''
+  c.set('oidcAuthEnv', oidcAuthEnv)
+}
+
 /**
  * Returns the environment variables for OIDC-auth middleware.
  */
 const getOidcAuthEnv = (c: Context) => {
   let oidcAuthEnv = c.get('oidcAuthEnv')
   if (oidcAuthEnv === undefined) {
-    oidcAuthEnv = env<OidcAuthEnv>(c)
-    if (oidcAuthEnv.OIDC_AUTH_SECRET === undefined) {
-      throw new HTTPException(500, { message: 'Session secret is not provided' })
-    }
-    if (oidcAuthEnv.OIDC_AUTH_SECRET.length < 32) {
-      throw new HTTPException(500, {
-        message: 'Session secrets must be at least 32 characters long',
-      })
-    }
-    if (oidcAuthEnv.OIDC_ISSUER === undefined) {
-      throw new HTTPException(500, { message: 'OIDC issuer is not provided' })
-    }
-    if (oidcAuthEnv.OIDC_CLIENT_ID === undefined) {
-      throw new HTTPException(500, { message: 'OIDC client ID is not provided' })
-    }
-    if (oidcAuthEnv.OIDC_CLIENT_SECRET === undefined) {
-      throw new HTTPException(500, { message: 'OIDC client secret is not provided' })
-    }
-    oidcAuthEnv.OIDC_REDIRECT_URI = oidcAuthEnv.OIDC_REDIRECT_URI ?? defaultOidcRedirectUri
-    if (!oidcAuthEnv.OIDC_REDIRECT_URI.startsWith('/')) {
-      try {
-        new URL(oidcAuthEnv.OIDC_REDIRECT_URI)
-      } catch (e) {
-        throw new HTTPException(500, {
-          message: 'The OIDC redirect URI is invalid. It must be a full URL or an absolute path',
-        })
-      }
-    }
-    oidcAuthEnv.OIDC_COOKIE_PATH = oidcAuthEnv.OIDC_COOKIE_PATH ?? defaultOidcAuthCookiePath
-    oidcAuthEnv.OIDC_COOKIE_NAME = oidcAuthEnv.OIDC_COOKIE_NAME ?? defaultOidcAuthCookieName
-    oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL =
-      oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL ?? `${defaultRefreshInterval}`
-    oidcAuthEnv.OIDC_AUTH_EXPIRES = oidcAuthEnv.OIDC_AUTH_EXPIRES ?? `${defaultExpirationInterval}`
-    oidcAuthEnv.OIDC_SCOPES = oidcAuthEnv.OIDC_SCOPES ?? ''
-    c.set('oidcAuthEnv', oidcAuthEnv)
+    setOidcAuthEnv(c)
+    oidcAuthEnv = c.get('oidcAuthEnv')
   }
   return oidcAuthEnv as Required<OidcAuthEnv>
 }


Reviews:
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T21:20:06Z
user: login: BarryThePenguin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T21:59:49Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T22:06:06Z
user: login: BarryThePenguin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T22:20:42Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T12:27:22Z
user: login: BarryThePenguin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T20:36:45Z
user: login: BarryThePenguin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T20:52:21Z
user: login: BarryThePenguin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T20:53:19Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T21:06:45Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T22:12:39Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T22:13:16Z
user: login: BarryThePenguin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T01:46:08Z
user: login: maemaemae3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:59:39Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T15:50:44Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:13:06Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:13:58Z
user: login: rajsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:14:51Z
user: login: hnw
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T09:14:17Z

Review comments:
diff_hunk: @@ -493,7 +495,7 @@ describe('processOAuthCallback()', () => {
     expect(res.status).toBe(302)
     expect(res.headers.get('set-cookie')).toMatch(
       new RegExp(
-        `${MOCK_COOKIE_NAME}=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`
+        `${MOCK_COOKIE_NAME}=[^;]+; Path=${defaultOidcAuthCookiePath}; HttpOnly; Secure`
path: packages/oidc-auth/src/index.test.ts
user: login: rajsite
body: Previously this test was relying on the side-effect that `process.env.OIDC_COOKIE_PATH` was mutated. This PR makes it so the env variables are no longer mutated
created_at: 2025-03-05T21:20:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: BarryThePenguin
body: Would it be simpler if `oidcAuthMiddleware()` accepted the `config` argument instead?

```suggestion
app.use(oidcAuthMiddleware({
  // Configure auth variables
});
```
created_at: 2025-03-05T21:59:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: rajsite
body: I think a separate function is still needed, you can end up on different code paths like the following example:
https://github.com/honojs/middleware/blob/cc26ddcae34d687faccbbdf04cd58d2cae4f388e/packages/oidc-auth/README.md#L60-L80

Where each function `revokeSession`, `processOauthCallback`, etc will internally call `getOidcAuthEnv` to resolve the configuration for the context.

The alternatives I thought of were add the config to each function (which would be clunky / duplicative) or have a separate function where you can more easily make sure it's the first function called in a middleware to configure the context of the request.
created_at: 2025-03-05T22:06:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: BarryThePenguin
body: Isn't that only because the middleware in your example is added later? If the middleware is added first, is it still a problem? That's exactly what's mentioned in the Readme, "Before other oidc-auth APIs are used"

I don't think the `setOidcAuthEnv()` function in it's current form is very ergonomic, especially making the developer manage the `next()` callback, etc.. If it could be used like this..

```ts
app.use(setOidcAuthEnv({ ... })
```

..that'd be ok. But then that's no different to using `oidcAuthMiddleware` for the same purpose.

Another alternative might be a factory function to create the middleware, bound with some config, for example;

```ts
 import { defineConfig } from '@hono/oidc-auth'

const { oidcAuthMiddleware, getAuth, revokeSession, processOAuthCallback } = defineConfig({
  // Configure auth variables
})
```

The rest of the PR looks good, avoiding mutating the environment variables is a good outcome 👍🏻 
created_at: 2025-03-05T22:20:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: rajsite
body: Thanks for your time in review! I'm just getting started with Hono so appreciate the feedback and discussion!

> Would it be simpler if oidcAuthMiddleware() accepted the config argument instead?
>> I think a separate function is still needed
>>> Isn't that only because the middleware in your example is added later? If the middleware is added first, is it still a problem?

The `oidcAuthMiddleware` as written wouldn't be used in all contexts like `/logout` and `/callback` in the example are intentionally not gated by the `oidcAuthMiddleware`. Pages that don't require auth like a public landing page or pages involved in the workflow itself like `/callback` shouldn't be gated. However, `setOidcAuthEnv` should be used in those contexts so for example `/callback` is aware of the current configuration state to finish the auth workflows but not gated on an already completed auth workflow.

I can update `setOidcAuthEnv` to be a middleware. I think I prefer that over the factory pattern approach since I need access to current context for some of the configuration and that seems like a well-known pattern with middleware, i.e. [Context access inside Middleware arguments](https://hono.dev/docs/guides/middleware#context-access-inside-middleware-arguments).

If keeping `setOidcAuthEnv` and making it middleware is a reasonable choice I can make that change 👍.

If reducing to just `oidcAuthMiddleware({ /*config*/ })` or the factory functions are preferred I think I would need some more implementation guidance based on the above concerns, i.e. for a single middleware how to make certain endpoints apply only config but not gating and for the middleware factory how one would access the context as part of the configuration.
created_at: 2025-03-10T12:27:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: BarryThePenguin
body: Ok, the extra context is helpful, thank you!

If you need access to context for some of the configuration then your current implementation is perfect 👍🏻
created_at: 2025-03-10T20:36:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,13 +60,39 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const setOidcAuthEnv = (c: Context, config: Partial<OidcAuthEnv>) => {
+  const oidcAuthEnv = c.get('oidcAuthEnv')
+  if (oidcAuthEnv !== undefined) {
+    throw new HTTPException(500, { message: 'OIDC Auth env is already configured' })
+  }
+  const _ = getOidcAuthEnv(c, config)
path: packages/oidc-auth/src/index.ts
user: login: BarryThePenguin
body: The only other thing I'd consider is moving the parts of `getOidcAuthEnv` that actually set the config into `setOidcAuthEnv`

Then have `getOidcAuthEnv` call `setOidcAuthEnv` when `oidcAuthEnv === undefined`
created_at: 2025-03-10T20:52:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: rajsite
body: I think I'm sold on making `setOidcAuthEnv` middleware and the usage cascade is:

1. Most folks use env variables for config
2. Some folks use the middleware, i.e. maybe different config on different routes or some global logic for the config
3. Very few folks use the middleware with context dependent config via the [`Context access inside Middleware arguments`](https://hono.dev/docs/guides/middleware#context-access-inside-middleware-arguments) pattern (which happens to be my need, but couldn't say if that's general)

With the feedback below of separating the `get` and `set` responsibilities I think that results in:
private `getOidcAuthEnv`, private `setOidcAuthEnv`, and a public `setOidcAuthEnvMiddleware` following the naming convention on `oidcAuthMiddleware`.

I think all that would address the feedback but let me know if there are any concerns.
created_at: 2025-03-10T21:06:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,13 +60,39 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const setOidcAuthEnv = (c: Context, config: Partial<OidcAuthEnv>) => {
+  const oidcAuthEnv = c.get('oidcAuthEnv')
+  if (oidcAuthEnv !== undefined) {
+    throw new HTTPException(500, { message: 'OIDC Auth env is already configured' })
+  }
+  const _ = getOidcAuthEnv(c, config)
path: packages/oidc-auth/src/index.ts
user: login: rajsite
body: Makes for a noisier diff but is a clearer separation of concerns, applied! 👍
created_at: 2025-03-10T22:12:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: rajsite
body: I've pushed the changes described, let me know if it works! 👍
created_at: 2025-03-10T22:13:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +140,19 @@ Note:
 If explicit logout is not required, the logout handler can be omitted.
 If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
 
+## Programmatically configure auth variables
+
+```typescript
+// Before other oidc-auth APIs are used
+app.use(async (c, next) => {
+  let config = {
+    // Configure auth variables
+  }
+  setOidcAuthEnv(c, config)
+  await next()
+});
path: packages/oidc-auth/README.md
user: login: BarryThePenguin
body: Looks good 👍🏻 
created_at: 2025-03-11T01:46:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,47 +60,84 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC middleware or functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const setOidcAuthEnvMiddleware = (config: Partial<OidcAuthEnv>) => {
+  return createMiddleware(async (c, next) => {
+    setOidcAuthEnv(c, config)
+    await next()
+  })
+}
path: packages/oidc-auth/src/index.ts
user: login: maemaemae3
body: Hi, sorry for seeing this late. I understood and like you idea!
How about setting `init` prefix like `initOidcAuthMiddleware`? The idea is based on `auth-js`.
https://github.com/honojs/middleware/tree/main/packages/auth-js#how-to-use

`setOidcAuthEnvMiddleware` makes it look like it can be reset afterwards I think.
created_at: 2025-03-12T14:59:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,47 +60,84 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC middleware or functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const setOidcAuthEnvMiddleware = (config: Partial<OidcAuthEnv>) => {
+  return createMiddleware(async (c, next) => {
+    setOidcAuthEnv(c, config)
+    await next()
+  })
+}
path: packages/oidc-auth/src/index.ts
user: login: rajsite
body: Makes sense! Applied!
created_at: 2025-03-12T15:50:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,47 +60,84 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC middleware or functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const initOidcAuthMiddleware = (config: Partial<OidcAuthEnv>) => {
+  return createMiddleware(async (c, next) => {
+    setOidcAuthEnv(c, config)
+    await next()
+  })
+}
+
+/**
+ * Configure the OIDC variables.
+ */
+const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
+  const currentOidcAuthEnv = c.get('oidcAuthEnv')
+  if (currentOidcAuthEnv !== undefined) {
+    throw new HTTPException(500, { message: 'OIDC Auth env is already configured' })
+  }
+  const ev = env<Readonly<OidcAuthEnv>>(c)
+  const oidcAuthEnv = {
+    OIDC_AUTH_SECRET: config?.OIDC_AUTH_SECRET ?? ev.OIDC_AUTH_SECRET,
+    OIDC_AUTH_REFRESH_INTERVAL: config?.OIDC_AUTH_REFRESH_INTERVAL ?? ev.OIDC_AUTH_REFRESH_INTERVAL,
+    OIDC_AUTH_EXPIRES: config?.OIDC_AUTH_EXPIRES ?? ev.OIDC_AUTH_EXPIRES,
+    OIDC_ISSUER: config?.OIDC_ISSUER ?? ev.OIDC_ISSUER,
+    OIDC_CLIENT_ID: config?.OIDC_CLIENT_ID ?? ev.OIDC_CLIENT_ID,
+    OIDC_CLIENT_SECRET: config?.OIDC_CLIENT_SECRET ?? ev.OIDC_CLIENT_SECRET,
+    OIDC_REDIRECT_URI: config?.OIDC_REDIRECT_URI ?? ev.OIDC_REDIRECT_URI,
+    OIDC_SCOPES: config?.OIDC_SCOPES ?? ev.OIDC_SCOPES,
+    OIDC_COOKIE_PATH: config?.OIDC_COOKIE_PATH ?? ev.OIDC_COOKIE_PATH,
+    OIDC_COOKIE_NAME: config?.OIDC_COOKIE_NAME ?? ev.OIDC_COOKIE_NAME,
+    OIDC_COOKIE_DOMAIN: config?.OIDC_COOKIE_DOMAIN ?? ev.OIDC_COOKIE_DOMAIN,
+  }
+  if (oidcAuthEnv.OIDC_AUTH_SECRET === undefined) {
+    throw new HTTPException(500, { message: 'Session secret is not provided' })
+  }
+  if (oidcAuthEnv.OIDC_AUTH_SECRET.length < 32) {
+    throw new HTTPException(500, {
+      message: 'Session secrets must be at least 32 characters long',
+    })
+  }
+  if (oidcAuthEnv.OIDC_ISSUER === undefined) {
+    throw new HTTPException(500, { message: 'OIDC issuer is not provided' })
+  }
+  if (oidcAuthEnv.OIDC_CLIENT_ID === undefined) {
+    throw new HTTPException(500, { message: 'OIDC client ID is not provided' })
+  }
+  if (oidcAuthEnv.OIDC_CLIENT_SECRET === undefined) {
+    throw new HTTPException(500, { message: 'OIDC client secret is not provided' })
+  }
+  oidcAuthEnv.OIDC_REDIRECT_URI = oidcAuthEnv.OIDC_REDIRECT_URI ?? defaultOidcRedirectUri
+  if (!oidcAuthEnv.OIDC_REDIRECT_URI.startsWith('/')) {
+    try {
+      new URL(oidcAuthEnv.OIDC_REDIRECT_URI)
+    } catch (e) {
+      throw new HTTPException(500, {
+        message: 'The OIDC redirect URI is invalid. It must be a full URL or an absolute path',
+      })
+    }
+  }
+  oidcAuthEnv.OIDC_COOKIE_PATH = oidcAuthEnv.OIDC_COOKIE_PATH ?? defaultOidcAuthCookiePath
+  oidcAuthEnv.OIDC_COOKIE_NAME = oidcAuthEnv.OIDC_COOKIE_NAME ?? defaultOidcAuthCookieName
+  oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL =
+    oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL ?? `${defaultRefreshInterval}`
+  oidcAuthEnv.OIDC_AUTH_EXPIRES = oidcAuthEnv.OIDC_AUTH_EXPIRES ?? `${defaultExpirationInterval}`
+  oidcAuthEnv.OIDC_SCOPES = oidcAuthEnv.OIDC_SCOPES ?? ''
+  c.set('oidcAuthEnv', oidcAuthEnv)
path: packages/oidc-auth/src/index.ts
user: login: rajsite
body: This logic is unchanged, just moved 
created_at: 2025-03-12T16:13:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,47 +60,84 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC middleware or functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const initOidcAuthMiddleware = (config: Partial<OidcAuthEnv>) => {
+  return createMiddleware(async (c, next) => {
+    setOidcAuthEnv(c, config)
+    await next()
+  })
+}
+
+/**
+ * Configure the OIDC variables.
+ */
+const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
+  const currentOidcAuthEnv = c.get('oidcAuthEnv')
+  if (currentOidcAuthEnv !== undefined) {
+    throw new HTTPException(500, { message: 'OIDC Auth env is already configured' })
+  }
+  const ev = env<Readonly<OidcAuthEnv>>(c)
path: packages/oidc-auth/src/index.ts
user: login: rajsite
body: Changed to take a Readonly reference to the environment variables to avoid accidental mutation
created_at: 2025-03-12T16:13:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,47 +60,84 @@ type OidcAuthEnv = {
   OIDC_COOKIE_DOMAIN?: string
 }
 
+/**
+ * Configure the OIDC variables programmatically.
+ * If used, should be called before any other OIDC middleware or functions for the Hono context.
+ * Unconfigured values will fallback to environment variables.
+ */
+export const initOidcAuthMiddleware = (config: Partial<OidcAuthEnv>) => {
+  return createMiddleware(async (c, next) => {
+    setOidcAuthEnv(c, config)
+    await next()
+  })
+}
+
+/**
+ * Configure the OIDC variables.
+ */
+const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
+  const currentOidcAuthEnv = c.get('oidcAuthEnv')
+  if (currentOidcAuthEnv !== undefined) {
+    throw new HTTPException(500, { message: 'OIDC Auth env is already configured' })
+  }
+  const ev = env<Readonly<OidcAuthEnv>>(c)
+  const oidcAuthEnv = {
+    OIDC_AUTH_SECRET: config?.OIDC_AUTH_SECRET ?? ev.OIDC_AUTH_SECRET,
+    OIDC_AUTH_REFRESH_INTERVAL: config?.OIDC_AUTH_REFRESH_INTERVAL ?? ev.OIDC_AUTH_REFRESH_INTERVAL,
+    OIDC_AUTH_EXPIRES: config?.OIDC_AUTH_EXPIRES ?? ev.OIDC_AUTH_EXPIRES,
+    OIDC_ISSUER: config?.OIDC_ISSUER ?? ev.OIDC_ISSUER,
+    OIDC_CLIENT_ID: config?.OIDC_CLIENT_ID ?? ev.OIDC_CLIENT_ID,
+    OIDC_CLIENT_SECRET: config?.OIDC_CLIENT_SECRET ?? ev.OIDC_CLIENT_SECRET,
+    OIDC_REDIRECT_URI: config?.OIDC_REDIRECT_URI ?? ev.OIDC_REDIRECT_URI,
+    OIDC_SCOPES: config?.OIDC_SCOPES ?? ev.OIDC_SCOPES,
+    OIDC_COOKIE_PATH: config?.OIDC_COOKIE_PATH ?? ev.OIDC_COOKIE_PATH,
+    OIDC_COOKIE_NAME: config?.OIDC_COOKIE_NAME ?? ev.OIDC_COOKIE_NAME,
+    OIDC_COOKIE_DOMAIN: config?.OIDC_COOKIE_DOMAIN ?? ev.OIDC_COOKIE_DOMAIN,
+  }
path: packages/oidc-auth/src/index.ts
user: login: rajsite
body: fallback to env variables when not provided in config
created_at: 2025-03-12T16:14:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-02-24T06:36:39Z
body: ###  🦋  Changeset detected

Latest commit: 0ae0742beddf7be778af5fd22f11af9bd086b8b4

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name            | Type  |
| --------------- | ----- |
| @hono/oidc-auth | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/rajsite/middleware/new/set-oidc-auth-env?filename=.changeset/pretty-coins-join.md&value=---%0A%22%40hono%2Foidc-auth%22%3A%20patch%0A---%0A%0Afeat(%40hono%2Foidc-auth)%20Add%20setOidcAuthEnvMiddleware%20and%20avoid%20mutating%20environment%20variables%0A)


author_association: NONE
user: login: rajsite
created_at: 2025-02-24T14:00:44Z
body: @hnw would appreciate your review of this PR when you have a chance 🙂
author_association: CONTRIBUTOR
user: login: rajsite
created_at: 2025-03-05T21:21:40Z
body: Merged vitest changes. Would still appreciate a review / feedback @hnw (fyi previous reviewers for oidc-auth @yusukebe @maemaemae3 )
author_association: CONTRIBUTOR
user: login: rajsite
created_at: 2025-03-12T16:16:50Z
body: @hnw Sorry for the repeated mentions but I added some comments to help review. Please let me know if you have any concerns with the direction, etc an I can address them quickly! I appreciate your time and for sharing the plugin!
author_association: CONTRIBUTOR
user: login: hnw
created_at: 2025-03-16T09:12:39Z
body: Apologies for the delay! I've reviewed this PR and it's excellent work.

You've elegantly addressed #978 and #979. The `initOidcAuthMiddleware` implementation is clean and leverages Hono's middleware system well, enabling flexible configuration.

Thank you for this valuable contribution!

author_association: CONTRIBUTOR
user: login: yusukebe
created_at: 2025-03-16T09:57:05Z
body: @rajsite @maemaemae3 @BarryThePenguin @hnw Thank you! I'll merge this PR into the main and release a new version.
author_association: MEMBER
user: login: rajsite
created_at: 2025-03-16T17:42:42Z
body: Thank you @hnw and @yusukebe  I appreciate the kind words!
author_association: CONTRIBUTOR