Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): Added code flow support for OIDC flow. #1220

Merged
merged 17 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
public readonly providerId: string;
public readonly issuer: string;
public readonly clientId: string;
public readonly clientSecret: string;
public readonly clientSecret?: string;
public readonly responseType: OAuthResponseType;

/**
Expand Down Expand Up @@ -799,46 +799,51 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
'"OIDCAuthProviderConfig.clientSecret" must be a valid string.',
);
}
if (typeof options.responseType !== 'undefined') {
let idTokenType = false;
let codeType = false;
if (validator.isNonNullObject(options.responseType) && typeof options.responseType !== 'undefined') {
let idTokenType;
xil222 marked this conversation as resolved.
Show resolved Hide resolved
let codeType;
let setIdTokenType = false;
let setCodeType = false;
for (const responseTypeKey in options.responseType) {
if (!(responseTypeKey in validResponseTypes)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${responseTypeKey}" is not a valid OAuthResponseType parameter.`,
);
} else {
if (responseTypeKey == 'idToken') {
if (responseTypeKey === 'idToken') {
if (!validator.isBoolean(options.responseType.idToken)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"OIDCAuthProviderConfig.responseType.${responseTypeKey}" must be a boolean.`,
'"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.',
);
}
if (options.responseType && options.responseType.idToken) {
if (typeof options.responseType.idToken !== 'undefined') {
idTokenType = options.responseType.idToken;
setIdTokenType = true;
}
} else if (responseTypeKey == 'code') {
} else if (responseTypeKey === 'code') {
if (!validator.isBoolean(options.responseType.code)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"OIDCAuthProviderConfig.responseType.${responseTypeKey}" must be a boolean.`,
'"OIDCAuthProviderConfig.responseType.code" must be a boolean.',
);
}
if (options.responseType && options.responseType.code) {
if (typeof options.responseType.code !== 'undefined') {
codeType = options.responseType.code;
setCodeType = true;
}
}
}
}

// Exact one of OAuth response type needs to be set to true.
if ((idTokenType && codeType) ||
(!idTokenType && !codeType)) {
// Only one of OAuth response types can be set to true.
if ((setIdTokenType && setCodeType) &&
((idTokenType && codeType) ||
(!idTokenType && !codeType))) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_OAUTH_RESPONSETYPE,
'Only exact one of the OAuth response types has to be set to true.',
'Only exactly one OAuth responseType should be set to true.',
);
}
// If code flow is enabled, client secret must be provided.
Expand Down Expand Up @@ -891,10 +896,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
if (typeof response.responseType !== 'undefined') {
this.responseType = response.responseType;
} else {
const responseType = {
idToken: true,
}
this.responseType = responseType;
this.responseType = { idToken: true };
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ export class AuthClientErrorCode {
};
public static INVALID_OAUTH_RESPONSETYPE = {
code: 'invalid-oauth-responsetype',
message: 'The oauth response type object must set exact one response type to true',
message: 'Only exactly one OAuth responseType should be set to true.',
};
public static INVALID_SESSION_COOKIE_DURATION = {
code: 'invalid-session-cookie-duration',
Expand Down
36 changes: 33 additions & 3 deletions test/integration/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,7 @@ describe('admin.auth', () => {
};
// Only above fields should be modified.
const modifiedConfigOptions = {
providerId: authProviderConfig1.providerId,
displayName: 'OIDC_DISPLAY_NAME3',
hiranya911 marked this conversation as resolved.
Show resolved Hide resolved
enabled: false,
issuer: 'https://oidc.com/issuer3',
Expand All @@ -1751,12 +1752,41 @@ describe('admin.auth', () => {
};
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges)
.then((config) => {
const modifiedConfig = deepExtend(
{ providerId: authProviderConfig1.providerId }, modifiedConfigOptions);
assertDeepEqualUnordered(modifiedConfig, config);
assertDeepEqualUnordered(modifiedConfigOptions, config);
});
});

it('updateProviderConfig() with invalid oauth response type should be rejected', () => {
const deltaChanges = {
displayName: 'OIDC_DISPLAY_NAME4',
enabled: false,
issuer: 'https://oidc.com/issuer4',
clientId: 'CLIENT_ID4',
clientSecret: 'CLIENT_SECRET',
responseType: {
idToken: false,
code: false,
},
};
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges).
should.eventually.be.rejected.and.have.property('code', 'auth/invalid-oauth-responsetype');
});

it('updateProviderConfig() code flow with no client secret should be rejected', () => {
const deltaChanges = {
displayName: 'OIDC_DISPLAY_NAME5',
enabled: false,
issuer: 'https://oidc.com/issuer5',
clientId: 'CLIENT_ID5',
responseType: {
idToken: false,
code: true,
},
};
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges).
should.eventually.be.rejected.and.have.property('code', 'auth/missing-oauth-client-secret');
});

it('deleteProviderConfig() successfully deletes an existing OIDC config', () => {
return admin.auth().deleteProviderConfig(authProviderConfig1.providerId).then(() => {
return admin.auth().getProviderConfig(authProviderConfig1.providerId)
Expand Down
37 changes: 29 additions & 8 deletions test/unit/auth/auth-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,12 +795,13 @@ describe('OIDCConfig', () => {
});

it('should set readonly property default responseType', () => {
delete serverResponse.clientSecret;
delete serverResponse.responseType;
const testResponse = deepCopy(serverResponse);
delete testResponse.clientSecret;
delete testResponse.responseType;
const expectedResponseType = {
idToken: true,
};
const testConfig = new OIDCConfig(serverResponse);
const testConfig = new OIDCConfig(testResponse);
expect(testConfig.responseType).to.deep.equal(expectedResponseType);
});

Expand Down Expand Up @@ -943,32 +944,52 @@ describe('OIDCConfig', () => {

it('should throw on OAuth responseType contains invalid parameters', () => {
const invalidRequest = deepCopy(clientRequest) as any;
invalidRequest.responseType.token = true;
invalidRequest.responseType.unknownField = true;
expect(() => OIDCConfig.validate(invalidRequest, true))
.to.throw('"token" is not a valid OAuthResponseType parameter.');
.to.throw('"unknownField" is not a valid OAuthResponseType parameter.');
});

it('should not throw when exact one OAuth responseType is true', () => {
it('should not throw when exactly one OAuth responseType is true', () => {
const validRequest = deepCopy(clientRequest) as any;
validRequest.responseType.code = false;
validRequest.responseType.idToken = true;
expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw();
});

it('should not throw when only idToken responseType is set to true', () => {
const validRequest = deepCopy(clientRequest) as any;
const validResponseType = { idToken: true };
validRequest.responseType = validResponseType;
xil222 marked this conversation as resolved.
Show resolved Hide resolved
expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw();
});

it('should not throw when only code responseType is set to true', () => {
const validRequest = deepCopy(clientRequest) as any;
const validResponseType = { code: true };
validRequest.responseType = validResponseType;
expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw();
});

it('should throw on two OAuth responseTypes set to true', () => {
xil222 marked this conversation as resolved.
Show resolved Hide resolved
const invalidRequest = deepCopy(clientRequest) as any;
invalidRequest.responseType.idToken = true;
invalidRequest.responseType.code = true;
expect(() => OIDCConfig.validate(invalidRequest, true))
.to.throw('Only exact one of the OAuth response types has to be set to true.');
.to.throw('Only exactly one OAuth responseType should be set to true.');
});

it('should throw on no OAuth responseType set to true', () => {
const invalidRequest = deepCopy(clientRequest) as any;
invalidRequest.responseType.idToken = false;
invalidRequest.responseType.code = false;
expect(() => OIDCConfig.validate(invalidRequest, true))
.to.throw('Only exact one of the OAuth response types has to be set to true.');
.to.throw('Only exactly one OAuth responseType should be set to true.');
});

it('should not throw when responseType is empty', () => {
const testRequest = deepCopy(clientRequest) as any;
testRequest.responseType = {};
expect(() => OIDCConfig.validate(testRequest, true)).not.to.throw();
});

it('should throw on no client secret when OAuth responseType code flow set to true', () => {
Expand Down