Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
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
8 changes: 8 additions & 0 deletions etc/firebase-admin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,23 @@ export namespace auth {
export interface MultiFactorUpdateSettings {
enrolledFactors: UpdateMultiFactorInfoRequest[] | null;
}
export interface OAuthResponseType {
code?: boolean;
idToken?: boolean;
}
export interface OIDCAuthProviderConfig extends AuthProviderConfig {
clientId: string;
clientSecret?: string;
issuer: string;
responseType?: OAuthResponseType;
}
export interface OIDCUpdateAuthProviderRequest {
clientId?: string;
clientSecret?: string;
displayName?: string;
enabled?: boolean;
issuer?: string;
responseType?: OAuthResponseType;
}
export interface PhoneIdentifier {
// (undocumented)
Expand Down
83 changes: 83 additions & 0 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import MultiFactorConfigState = auth.MultiFactorConfigState;
import AuthFactorType = auth.AuthFactorType;
import EmailSignInProviderConfig = auth.EmailSignInProviderConfig;
import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig;
import OAuthResponseType = auth.OAuthResponseType;
import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig;

/** A maximum of 10 test phone number / code pairs can be configured. */
Expand Down Expand Up @@ -75,6 +76,8 @@ export interface OIDCConfigServerRequest {
issuer?: string;
displayName?: string;
enabled?: boolean;
clientSecret?: string;
responseType?: OAuthResponseType;
[key: string]: any;
}

Expand All @@ -87,6 +90,8 @@ export interface OIDCConfigServerResponse {
issuer?: string;
displayName?: string;
enabled?: boolean;
clientSecret?: string;
responseType?: OAuthResponseType;
}

/** The server side email configuration request interface. */
Expand Down Expand Up @@ -650,6 +655,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
public readonly providerId: string;
public readonly issuer: string;
public readonly clientId: string;
public readonly clientSecret?: string;
public readonly responseType: OAuthResponseType;

/**
* Converts a client side request to a OIDCConfigServerRequest which is the format
Expand All @@ -676,6 +683,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
request.displayName = options.displayName;
request.issuer = options.issuer;
request.clientId = options.clientId;
if (typeof options.clientSecret !== 'undefined') {
request.clientSecret = options.clientSecret;
}
if (typeof options.responseType !== 'undefined') {
request.responseType = options.responseType;
}
return request;
}

Expand Down Expand Up @@ -715,6 +728,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
providerId: true,
clientId: true,
issuer: true,
clientSecret: true,
responseType: true,
};
const validResponseTypes = {
idToken: true,
code: true,
};
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
Expand Down Expand Up @@ -773,6 +792,61 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
'"OIDCAuthProviderConfig.displayName" must be a valid string.',
);
}
if (typeof options.clientSecret !== 'undefined' &&
!validator.isNonEmptyString(options.clientSecret)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"OIDCAuthProviderConfig.clientSecret" must be a valid string.',
);
}
if (validator.isNonNullObject(options.responseType) && typeof options.responseType !== 'undefined') {
Object.keys(options.responseType).forEach((key) => {
if (!(key in validResponseTypes)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid OAuthResponseType parameter.`,
);
}
});

const idToken = options.responseType.idToken;
if (typeof idToken !== 'undefined') {
if (!validator.isBoolean(idToken)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.',
);
}
}

const code = options.responseType.code;
if (typeof code !== 'undefined') {
if (!validator.isBoolean(code)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"OIDCAuthProviderConfig.responseType.code" must be a boolean.',
);
}

// If code flow is enabled, client secret must be provided.
if (typeof options.clientSecret === 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.MISSING_OAUTH_CLIENT_SECRET,
'The OAuth configuration client secret is required to enable OIDC code flow.',
);
}
}

const allKeys = Object.keys(options.responseType).length;
const enabledCount = Object.values(options.responseType).filter(Boolean).length;
// Only one of OAuth response types can be set to true.
if (allKeys > 1 && enabledCount != 1) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_OAUTH_RESPONSETYPE,
'Only exactly one OAuth responseType should be set to true.',
);
}
}
}

/**
Expand Down Expand Up @@ -806,6 +880,13 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
// When enabled is undefined, it takes its default value of false.
this.enabled = !!response.enabled;
this.displayName = response.displayName;

if (typeof response.clientSecret !== 'undefined') {
this.clientSecret = response.clientSecret;
}
if (typeof response.responseType !== 'undefined') {
this.responseType = response.responseType;
}
}

/** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */
Expand All @@ -816,6 +897,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
providerId: this.providerId,
issuer: this.issuer,
clientId: this.clientId,
clientSecret: this.clientSecret,
responseType: this.responseType,
};
}
}
40 changes: 40 additions & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,25 @@ export namespace auth {
callbackURL?: string;
}

/**
* The interface representing OIDC provider's response object for OAuth
* authorization flow.
* We need either of them to be true, there are two cases:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment could be improved. Suggest:

" * One of the following must be true:

  • If code is set to true, then we are doing code flow.
  • If dToken is set to true, then we are doing ID token flow."

(Assuming that backticks are rendered as code font, and that "ID token flow" is a thing, separate from the literal idToken flag.

* If set code to true, then we are doing code flow.
* If set idToken to true, then we are doing idToken flow.
*/
export interface OAuthResponseType {
/**
* Whether ID token is returned from IdP's authorization endpoint.
*/
idToken?: boolean;

/**
* Whether authorization code is returned from IdP's authorization endpoint.
*/
code?: boolean;
}

/**
* The [OIDC](https://openid.net/specs/openid-connect-core-1_0-final.html) Auth
* provider configuration interface. An OIDC provider can be created via
Expand Down Expand Up @@ -1321,6 +1340,16 @@ export namespace auth {
* [spec](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).
*/
issuer: string;

/**
* The OIDC provider's client secret to enable OIDC code flow.
*/
clientSecret?: string;

/**
* The OIDC provider's response object for OAuth authorization flow.
*/
responseType?: OAuthResponseType;
}

/**
Expand Down Expand Up @@ -1403,6 +1432,17 @@ export namespace auth {
* configuration's value is not modified.
*/
issuer?: string;

/**
* The OIDC provider's client secret to enable OIDC code flow.
* If not provided, the existing configuration's value is not modified.
*/
clientSecret?: string;

/**
* The OIDC provider's response object for OAuth authorization flow.
*/
responseType?: OAuthResponseType;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ export class AuthClientErrorCode {
code: 'invalid-provider-uid',
message: 'The providerUid must be a valid provider uid string.',
};
public static INVALID_OAUTH_RESPONSETYPE = {
code: 'invalid-oauth-responsetype',
message: 'Only exactly one OAuth responseType should be set to true.',
};
public static INVALID_SESSION_COOKIE_DURATION = {
code: 'invalid-session-cookie-duration',
message: 'The session cookie duration must be a valid number in milliseconds ' +
Expand Down Expand Up @@ -593,6 +597,10 @@ export class AuthClientErrorCode {
code: 'missing-oauth-client-id',
message: 'The OAuth/OIDC configuration client ID must not be empty.',
};
public static MISSING_OAUTH_CLIENT_SECRET = {
code: 'missing-oauth-client-secret',
message: 'The OAuth configuration client secret is required to enable OIDC code flow.',
};
public static MISSING_PROVIDER_ID = {
code: 'missing-provider-id',
message: 'A valid provider ID must be provided in the request.',
Expand Down
76 changes: 59 additions & 17 deletions test/integration/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1334,12 +1334,21 @@ describe('admin.auth', () => {
enabled: true,
issuer: 'https://oidc.com/issuer1',
clientId: 'CLIENT_ID1',
responseType: {
idToken: true,
code: false,
},
};
const modifiedConfigOptions = {
displayName: 'OIDC_DISPLAY_NAME3',
enabled: false,
issuer: 'https://oidc.com/issuer3',
clientId: 'CLIENT_ID3',
clientSecret: 'CLIENT_SECRET',
responseType: {
idToken: false,
code: true,
},
};

before(function() {
Expand Down Expand Up @@ -1633,13 +1642,20 @@ describe('admin.auth', () => {
enabled: true,
issuer: 'https://oidc.com/issuer1',
clientId: 'CLIENT_ID1',
responseType: {
idToken: true,
},
};
const authProviderConfig2 = {
providerId: randomOidcProviderId(),
displayName: 'OIDC_DISPLAY_NAME2',
enabled: true,
issuer: 'https://oidc.com/issuer2',
clientId: 'CLIENT_ID2',
clientSecret: 'CLIENT_SECRET',
responseType: {
code: true,
},
};

const removeTempConfigs = (): Promise<any> => {
Expand Down Expand Up @@ -1706,39 +1722,65 @@ describe('admin.auth', () => {
});
});

it('updateProviderConfig() successfully overwrites an OIDC config', () => {
it('updateProviderConfig() successfully partially modifies an OIDC config', () => {
const deltaChanges = {
displayName: 'OIDC_DISPLAY_NAME3',
enabled: false,
issuer: 'https://oidc.com/issuer3',
clientId: 'CLIENT_ID3',
clientSecret: 'CLIENT_SECRET',
responseType: {
idToken: false,
code: true,
},
};
// Only above fields should be modified.
const modifiedConfigOptions = {
providerId: authProviderConfig1.providerId,
displayName: 'OIDC_DISPLAY_NAME3',
enabled: false,
issuer: 'https://oidc.com/issuer3',
clientId: 'CLIENT_ID3',
clientSecret: 'CLIENT_SECRET',
responseType: {
code: true,
},
};
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions)
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges)
.then((config) => {
const modifiedConfig = deepExtend(
{ providerId: authProviderConfig1.providerId }, modifiedConfigOptions);
assertDeepEqualUnordered(modifiedConfig, config);
assertDeepEqualUnordered(modifiedConfigOptions, config);
});
});

it('updateProviderConfig() successfully partially modifies an OIDC 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,
},
};
// Only above fields should be modified.
const modifiedConfigOptions = {
displayName: 'OIDC_DISPLAY_NAME4',
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/issuer4',
clientId: 'CLIENT_ID3',
issuer: 'https://oidc.com/issuer5',
clientId: 'CLIENT_ID5',
responseType: {
idToken: false,
code: true,
},
};
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges)
.then((config) => {
const modifiedConfig = deepExtend(
{ providerId: authProviderConfig1.providerId }, modifiedConfigOptions);
assertDeepEqualUnordered(modifiedConfig, config);
});
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', () => {
Expand Down
Loading