mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
Pass application_type as native (#252226)
We should be treated as a public client.
This commit is contained in:
committed by
GitHub
parent
4d19bfbe1c
commit
fa995da0cb
@@ -46,6 +46,28 @@ export const enum AuthorizationDeviceCodeErrorType {
|
||||
ExpiredToken = 'expired_token'
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic client registration specific error codes as specified in RFC 7591.
|
||||
*/
|
||||
export const enum AuthorizationRegistrationErrorType {
|
||||
/**
|
||||
* The value of one or more redirection URIs is invalid.
|
||||
*/
|
||||
InvalidRedirectUri = 'invalid_redirect_uri',
|
||||
/**
|
||||
* The value of one of the client metadata fields is invalid and the server has rejected this request.
|
||||
*/
|
||||
InvalidClientMetadata = 'invalid_client_metadata',
|
||||
/**
|
||||
* The software statement presented is invalid.
|
||||
*/
|
||||
InvalidSoftwareStatement = 'invalid_software_statement',
|
||||
/**
|
||||
* The software statement presented is not approved for use by this authorization server.
|
||||
*/
|
||||
UnapprovedSoftwareStatement = 'unapproved_software_statement'
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata about a protected resource.
|
||||
*/
|
||||
@@ -458,6 +480,18 @@ export interface IAuthorizationDeviceTokenErrorResponse extends IAuthorizationEr
|
||||
error: AuthorizationErrorType | AuthorizationDeviceCodeErrorType | string;
|
||||
}
|
||||
|
||||
export interface IAuthorizationRegistrationErrorResponse {
|
||||
/**
|
||||
* REQUIRED. Error code as specified in OAuth 2.0 or Dynamic Client Registration.
|
||||
*/
|
||||
error: AuthorizationRegistrationErrorType | string;
|
||||
|
||||
/**
|
||||
* OPTIONAL. Human-readable description of the error.
|
||||
*/
|
||||
error_description?: string;
|
||||
}
|
||||
|
||||
export interface IAuthorizationJWTClaims {
|
||||
/**
|
||||
* REQUIRED. JWT ID. Unique identifier for the token.
|
||||
@@ -653,6 +687,14 @@ export function isAuthorizationErrorResponse(obj: unknown): obj is IAuthorizatio
|
||||
return response.error !== undefined;
|
||||
}
|
||||
|
||||
export function isAuthorizationRegistrationErrorResponse(obj: unknown): obj is IAuthorizationRegistrationErrorResponse {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
const response = obj as IAuthorizationRegistrationErrorResponse;
|
||||
return response.error !== undefined;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
export function getDefaultMetadataForUrl(authorizationServer: URL): IRequiredAuthorizationServerMetadata & IRequiredAuthorizationServerMetadata {
|
||||
@@ -719,12 +761,26 @@ export async function fetchDynamicRegistration(serverMetadata: IAuthorizationSer
|
||||
`http://127.0.0.1:${DEFAULT_AUTH_FLOW_PORT}/`
|
||||
],
|
||||
scope: scopes?.join(AUTH_SCOPE_SEPARATOR),
|
||||
token_endpoint_auth_method: 'none'
|
||||
token_endpoint_auth_method: 'none',
|
||||
// https://openid.net/specs/openid-connect-registration-1_0.html
|
||||
application_type: 'native'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Registration failed: ${response.statusText}`);
|
||||
const result = await response.text();
|
||||
let errorDetails: string = result;
|
||||
|
||||
try {
|
||||
const errorResponse = JSON.parse(result);
|
||||
if (isAuthorizationRegistrationErrorResponse(errorResponse)) {
|
||||
errorDetails = `${errorResponse.error}${errorResponse.error_description ? `: ${errorResponse.error_description}` : ''}`;
|
||||
}
|
||||
} catch {
|
||||
// JSON parsing failed, use raw text
|
||||
}
|
||||
|
||||
throw new Error(`Registration to ${serverMetadata.registration_endpoint} failed: ${errorDetails}`);
|
||||
}
|
||||
|
||||
const registration = await response.json();
|
||||
|
||||
@@ -356,7 +356,8 @@ suite('OAuth', () => {
|
||||
test('fetchDynamicRegistration should throw error on non-OK response', async () => {
|
||||
fetchStub.resolves({
|
||||
ok: false,
|
||||
statusText: 'Bad Request'
|
||||
statusText: 'Bad Request',
|
||||
text: async () => 'Bad Request'
|
||||
} as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
@@ -367,7 +368,7 @@ suite('OAuth', () => {
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/Registration failed: Bad Request/
|
||||
/Registration to https:\/\/auth\.example\.com\/register failed: Bad Request/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -448,6 +449,212 @@ suite('OAuth', () => {
|
||||
const requestBody = JSON.parse(options.body as string);
|
||||
assert.deepStrictEqual(requestBody.grant_types, ['authorization_code', 'refresh_token', 'urn:ietf:params:oauth:grant-type:device_code']);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should throw error when registration endpoint is missing', async () => {
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
response_types_supported: ['code']
|
||||
// registration_endpoint is missing
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/Server does not support dynamic registration/
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should handle structured error response', async () => {
|
||||
const errorResponse = {
|
||||
error: 'invalid_client_metadata',
|
||||
error_description: 'The client metadata is invalid'
|
||||
};
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: false,
|
||||
text: async () => JSON.stringify(errorResponse)
|
||||
} as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/Registration to https:\/\/auth\.example\.com\/register failed: invalid_client_metadata: The client metadata is invalid/
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should handle structured error response without description', async () => {
|
||||
const errorResponse = {
|
||||
error: 'invalid_redirect_uri'
|
||||
};
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: false,
|
||||
text: async () => JSON.stringify(errorResponse)
|
||||
} as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/Registration to https:\/\/auth\.example\.com\/register failed: invalid_redirect_uri/
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should handle malformed JSON error response', async () => {
|
||||
fetchStub.resolves({
|
||||
ok: false,
|
||||
text: async () => 'Invalid JSON {'
|
||||
} as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/Registration to https:\/\/auth\.example\.com\/register failed: Invalid JSON \{/
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should include scopes in request when provided', async () => {
|
||||
const mockResponse = {
|
||||
client_id: 'generated-client-id',
|
||||
client_name: 'Test Client'
|
||||
};
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
} as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await fetchDynamicRegistration(serverMetadata, 'Test Client', ['read', 'write']);
|
||||
|
||||
// Verify request includes scopes
|
||||
const [, options] = fetchStub.firstCall.args;
|
||||
const requestBody = JSON.parse(options.body as string);
|
||||
assert.strictEqual(requestBody.scope, 'read write');
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should omit scope from request when not provided', async () => {
|
||||
const mockResponse = {
|
||||
client_id: 'generated-client-id',
|
||||
client_name: 'Test Client'
|
||||
};
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
} as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await fetchDynamicRegistration(serverMetadata, 'Test Client');
|
||||
|
||||
// Verify request does not include scope when not provided
|
||||
const [, options] = fetchStub.firstCall.args;
|
||||
const requestBody = JSON.parse(options.body as string);
|
||||
assert.strictEqual(requestBody.scope, undefined);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should handle empty scopes array', async () => {
|
||||
const mockResponse = {
|
||||
client_id: 'generated-client-id',
|
||||
client_name: 'Test Client'
|
||||
};
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
} as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await fetchDynamicRegistration(serverMetadata, 'Test Client', []);
|
||||
|
||||
// Verify request includes empty scope
|
||||
const [, options] = fetchStub.firstCall.args;
|
||||
const requestBody = JSON.parse(options.body as string);
|
||||
assert.strictEqual(requestBody.scope, '');
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should handle network fetch failure', async () => {
|
||||
fetchStub.rejects(new Error('Network error'));
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/Network error/
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should handle response.json() failure', async () => {
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: async () => {
|
||||
throw new Error('JSON parsing failed');
|
||||
}
|
||||
} as unknown as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/JSON parsing failed/
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchDynamicRegistration should handle response.text() failure for error cases', async () => {
|
||||
fetchStub.resolves({
|
||||
ok: false,
|
||||
text: async () => {
|
||||
throw new Error('Text parsing failed');
|
||||
}
|
||||
} as unknown as Response);
|
||||
|
||||
const serverMetadata: IAuthorizationServerMetadata = {
|
||||
issuer: 'https://auth.example.com',
|
||||
registration_endpoint: 'https://auth.example.com/register',
|
||||
response_types_supported: ['code']
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
async () => await fetchDynamicRegistration(serverMetadata, 'Test Client'),
|
||||
/Text parsing failed/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getResourceServerBaseUrlFromDiscoveryUrl', () => {
|
||||
|
||||
Reference in New Issue
Block a user