Skip to content

Commit 0bff854

Browse files
committed
fix(dataverse): reject port-qualified origins, refuse reconnect without a host
- resolve: an explicit port survived into the origin, which becomes both the OAuth audience and the API base. Providers publish their resource on the default port, so a port-qualified origin names a resource nobody serves. `URL` drops the port only when it is the scheme default, so anything left is explicit and now rejected. - reconnect: the credential-detail Reconnect action calls the connect mutation directly with no environment URL, so a resource-scoped service failed with a validator error about a field that surface never shows. It now refuses up front with the route that works. The check runs before the draft is written, so a refusal leaves nothing behind. `WorkspaceCredential` carries no granted scopes, so the origin cannot be read back off the credential here — that is the follow-up already noted on the PR.
1 parent f881696 commit 0bff854

3 files changed

Lines changed: 32 additions & 0 deletions

File tree

apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useRouter } from 'next/navigation'
1919
import { SaveDiscardChips } from '@/components/settings/save-discard-actions'
2020
import { writeOAuthReturnContext } from '@/lib/credentials/client-state'
2121
import { resolveCredentialDisplay } from '@/lib/integrations'
22+
import { getServiceConfigByProviderId } from '@/lib/oauth/utils'
2223
import {
2324
AddPeopleModal,
2425
CredentialDetailHeading,
@@ -113,6 +114,19 @@ export function ConnectedCredentialDetail({
113114
const handleReconnectOAuth = async () => {
114115
if (!credential || credential.type !== 'oauth' || !credential.providerId || !workspaceId) return
115116
try {
117+
/**
118+
* A reconnect must return to the environment the credential belongs to,
119+
* and this surface has no way to supply it — `WorkspaceCredential` carries
120+
* no granted scopes to read the origin back from. Checked before the draft
121+
* is written so a refusal leaves nothing behind.
122+
*/
123+
const resourceConfig = getServiceConfigByProviderId(credential.providerId)?.resourceUrl
124+
if (resourceConfig) {
125+
throw new Error(
126+
`Reconnecting ${credential.displayName} needs its ${resourceConfig.title}. Disconnect it here, then connect the account again to enter one.`
127+
)
128+
}
129+
116130
await createDraft.mutateAsync({
117131
workspaceId,
118132
providerId: credential.providerId,

apps/sim/lib/oauth/resource-url.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ describe('resolveResourceOrigin', () => {
4141
expect(resolveResourceOrigin('http://myorg.crm.dynamics.com', config).ok).toBe(false)
4242
})
4343

44+
it('rejects an explicit port, which no provider serves its resource on', () => {
45+
expect(resolveResourceOrigin('https://myorg.crm.dynamics.com:8443', config).ok).toBe(false)
46+
})
47+
48+
it('accepts the default https port, which URL drops from the origin', () => {
49+
const result = resolveResourceOrigin('https://myorg.crm.dynamics.com:443', config)
50+
expect(result).toEqual({ ok: true, origin: 'https://myorg.crm.dynamics.com' })
51+
})
52+
4453
it('rejects embedded credentials', () => {
4554
expect(resolveResourceOrigin('https://u:p@myorg.crm.dynamics.com', config).ok).toBe(false)
4655
})

apps/sim/lib/oauth/resource-url.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ export function resolveResourceOrigin(
4040
if (parsed.username || parsed.password) {
4141
return { ok: false, error: `${config.title} must not contain credentials` }
4242
}
43+
/**
44+
* `URL` drops the port only when it is the scheme default, so anything left
45+
* here is explicit. The origin becomes an OAuth audience and the base for
46+
* every API request, and providers publish their resource on the default
47+
* port — a port-qualified origin would be a resource nobody serves.
48+
*/
49+
if (parsed.port) {
50+
return { ok: false, error: `${config.title} must not include a port` }
51+
}
4352

4453
/**
4554
* The allowlist is the whole security control here: no private address,

0 commit comments

Comments
 (0)