Skip to content

Commit e1abd36

Browse files
committed
fix: allow opting in to insecure OAuth requests
oauth4webapi enforces HTTPS on every request, so the auth-code + PKCE + DPoP flow could not talk to a local Community Solid Server over plain HTTP. Rather than guessing which issuers are loopback, expose a global switch consumers toggle themselves. InsecureConfiguration.allow() is deprecated on purpose so the security implication shows up at the call site. Also replaces the unconditional allow in BearerTokenProvider, and covers ClientCredentialsTokenProvider, which had no way to reach a local issuer at all.
1 parent 88eb0e5 commit e1abd36

6 files changed

Lines changed: 47 additions & 12 deletions

File tree

index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
}
1616
</script>
1717
<script type="module">
18-
import { DPoPTokenProvider, ReactiveFetchManager } from "./dist/mod.js"
18+
import { DPoPTokenProvider, InsecureConfiguration, ReactiveFetchManager } from "./dist/mod.js"
1919
import "./dist/registerElements.js"
2020

21+
/* Demo talks to a local Community Solid Server over plain HTTP */
22+
InsecureConfiguration.allow()
23+
2124
/* Reactive fetch infrastructure */
2225
const ui = document.querySelector("authorization-code-flow")
2326
const issuerUi = document.querySelector("idp-picker")

src/BearerTokenProvider.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import * as oauth from "oauth4webapi"
22
import { GetCodeCallback } from "./GetCodeCallback.js"
33
import { TokenProvider } from "./TokenProvider.js"
4-
5-
// TODO: Configure properly for insecure localhost only
6-
const oauthAllowInsecureRequests = true
4+
import { InsecureConfiguration } from "./InsecureConfiguration.js"
75

86
export class BearerTokenProvider implements TokenProvider {
97
readonly #getCode: GetCodeCallback
@@ -43,12 +41,12 @@ export class BearerTokenProvider implements TokenProvider {
4341
async upgrade(request: Request): Promise<Request> {
4442
const issuer = await this.#getIssuer(request)
4543

46-
const discoveryResponse = await oauth.discoveryRequest(issuer, {[oauth.allowInsecureRequests]: oauthAllowInsecureRequests})
44+
const discoveryResponse = await oauth.discoveryRequest(issuer, InsecureConfiguration.requestOptions)
4745
const authorizationServer = await oauth.processDiscoveryResponse(issuer, discoveryResponse)
4846

4947
const callbackUri = await this.#getCallback(request)
5048

51-
const registrationResponse = await oauth.dynamicClientRegistrationRequest(authorizationServer, {redirect_uris: [callbackUri]}, {[oauth.allowInsecureRequests]: oauthAllowInsecureRequests})
49+
const registrationResponse = await oauth.dynamicClientRegistrationRequest(authorizationServer, {redirect_uris: [callbackUri]}, InsecureConfiguration.requestOptions)
5250
const clientRegistration = await oauth.processDynamicClientRegistrationResponse(registrationResponse)
5351
const [registeredRedirectUri] = clientRegistration.redirect_uris as string[]
5452
const [registeredResponseType] = clientRegistration.response_types as string[]
@@ -84,7 +82,7 @@ export class BearerTokenProvider implements TokenProvider {
8482
clientAuth = authenticationMethod(clientSecret)
8583
}
8684

87-
const tokenResponse = await oauth.authorizationCodeGrantRequest(authorizationServer, clientRegistration, clientAuth, authorizationCodeParams, callbackUri, codeVerifier, {[oauth.allowInsecureRequests]: oauthAllowInsecureRequests})
85+
const tokenResponse = await oauth.authorizationCodeGrantRequest(authorizationServer, clientRegistration, clientAuth, authorizationCodeParams, callbackUri, codeVerifier, InsecureConfiguration.requestOptions)
8886

8987
// jwt nonce missing in igrant
9088
// const tokenResult = await oauth.processAuthorizationCodeResponse(authorizationServer, clientRegistration, tokenResponse, {expectedNonce: nonce})

src/ClientCredentialsTokenProvider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as oauth from "oauth4webapi"
22
import { AuthorizationServer } from "oauth4webapi"
33
import * as DPoP from "dpop"
44
import type { TokenProvider } from "./TokenProvider.js"
5+
import { InsecureConfiguration } from "./InsecureConfiguration.js"
56

67
export class ClientCredentialsTokenProvider implements TokenProvider {
78
constructor(private clientId: string, private clientSecret: string) {
@@ -35,7 +36,8 @@ export class ClientCredentialsTokenProvider implements TokenProvider {
3536
const issuer = await this.#getIssuer(request)
3637

3738
const discoveryResponse = await oauth.discoveryRequest(issuer, {
38-
signal: request.signal
39+
signal: request.signal,
40+
...InsecureConfiguration.requestOptions
3941
})
4042
const authorizationServer = await oauth.processDiscoveryResponse(issuer, discoveryResponse)
4143

@@ -46,7 +48,8 @@ export class ClientCredentialsTokenProvider implements TokenProvider {
4648

4749
const tokenResponse = await oauth.clientCredentialsGrantRequest(authorizationServer, clientRegistration, this.getClientAuth(authorizationServer, clientRegistration), {scope: "webid"}, {
4850
DPoP: dpop,
49-
signal: request.signal
51+
signal: request.signal,
52+
...InsecureConfiguration.requestOptions
5053
})
5154

5255
const tokenResult = await oauth.processClientCredentialsResponse(authorizationServer, clientRegistration, tokenResponse)

src/DPoPTokenProvider.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as DPoP from "dpop"
33
import type { GetCodeCallback } from "./GetCodeCallback.js"
44
import type { TokenProvider } from "./TokenProvider.js"
55
import type { GetIssuerCallback } from "./GetIssuerCallback.js"
6+
import { InsecureConfiguration } from "./InsecureConfiguration.js"
67

78
export class DPoPTokenProvider implements TokenProvider {
89
readonly #getCode: GetCodeCallback
@@ -22,10 +23,10 @@ export class DPoPTokenProvider implements TokenProvider {
2223
async upgrade(request: Request): Promise<Request> {
2324
const issuer = await this.#getIssuer(request)
2425

25-
const discoveryResponse = await oauth.discoveryRequest(issuer, {signal: request.signal})
26+
const discoveryResponse = await oauth.discoveryRequest(issuer, {signal: request.signal, ...InsecureConfiguration.requestOptions})
2627
const authorizationServer = await oauth.processDiscoveryResponse(issuer, discoveryResponse)
2728

28-
const registrationResponse = await oauth.dynamicClientRegistrationRequest(authorizationServer, {redirect_uris: [this.#callbackUri]}, {signal: request.signal})
29+
const registrationResponse = await oauth.dynamicClientRegistrationRequest(authorizationServer, {redirect_uris: [this.#callbackUri]}, {signal: request.signal, ...InsecureConfiguration.requestOptions})
2930
const clientRegistration = await oauth.processDynamicClientRegistrationResponse(registrationResponse)
3031
const [registeredRedirectUri] = clientRegistration.redirect_uris as string[]
3132
const [registeredResponseType] = clientRegistration.response_types as string[]
@@ -79,7 +80,7 @@ export class DPoPTokenProvider implements TokenProvider {
7980
}
8081
}
8182

82-
const tokenResponse = await oauth.authorizationCodeGrantRequest(authorizationServer, clientRegistration, this.getClientAuth(authorizationServer.issuer, clientRegistration), authorizationCodeParams, this.#callbackUri, authorizationServer.code_challenge_methods_supported !== undefined ? codeVerifier : oauth.nopkce, {DPoP: dpop, signal: request.signal})
83+
const tokenResponse = await oauth.authorizationCodeGrantRequest(authorizationServer, clientRegistration, this.getClientAuth(authorizationServer.issuer, clientRegistration), authorizationCodeParams, this.#callbackUri, authorizationServer.code_challenge_methods_supported !== undefined ? codeVerifier : oauth.nopkce, {DPoP: dpop, signal: request.signal, ...InsecureConfiguration.requestOptions})
8384

8485
const tokenResult = await oauth.processAuthorizationCodeResponse(authorizationServer, clientRegistration, tokenResponse, {expectedNonce: this.nonceVerificationOverride(authorizationServer.issuer, nonce)})
8586

src/InsecureConfiguration.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as oauth from "oauth4webapi"
2+
3+
/**
4+
* Global opt-in switch for oauth4webapi's HTTPS enforcement.
5+
*
6+
* @remarks
7+
* Enforcement is on by default. Consumers targeting a plain HTTP issuer, such as a
8+
* Community Solid Server on `http://localhost:3000`, opt out for themselves.
9+
*/
10+
export class InsecureConfiguration {
11+
static #allowed = false
12+
13+
/**
14+
* Allows OAuth requests over plain HTTP.
15+
*
16+
* @remarks
17+
* Deprecated on purpose, so that consumers see the security implication at the call site.
18+
*
19+
* @deprecated Only ever call this in local development.
20+
*/
21+
static allow() {
22+
console.error("Insecure requests allowed for oauth4webapi")
23+
this.#allowed = true
24+
}
25+
26+
static get requestOptions(): {[oauth.allowInsecureRequests]?: boolean} {
27+
return {[oauth.allowInsecureRequests]: this.#allowed}
28+
}
29+
}

src/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from "./IdpPicker.js"
1414
export * from "./WebIdPicker.js"
1515
export * from "./WebIdRequestCancelledError.js"
1616
export * from "./IssuerRequestCancelledError.js"
17+
export * from "./InsecureConfiguration.js"

0 commit comments

Comments
 (0)