Pass application_type as native (#252226)

We should be treated as a public client.
This commit is contained in:
Tyler James Leonhardt
2025-06-23 13:32:59 -07:00
committed by GitHub
parent 4d19bfbe1c
commit fa995da0cb
2 changed files with 267 additions and 4 deletions
+58 -2
View File
@@ -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();
+209 -2
View File
@@ -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', () => {