Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
92 changes: 92 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,63 @@ 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 (typeof options.responseType !== 'undefined') {
let idTokenType = false;
let codeType = 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 (!validator.isBoolean(options.responseType.idToken)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"OIDCAuthProviderConfig.responseType.${responseTypeKey}" must be a boolean.`,
);
}
if (options.responseType && options.responseType.idToken) {
idTokenType = options.responseType.idToken;
}
} else if (responseTypeKey == 'code') {
if (!validator.isBoolean(options.responseType.code)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"OIDCAuthProviderConfig.responseType.${responseTypeKey}" must be a boolean.`,
);
}
if (options.responseType && options.responseType.code) {
codeType = options.responseType.code;
}
}
}
}

// Exact one of OAuth response type needs to be set to true.
if ((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.',
);
}
// If code flow is enabled, client secret must be provided.
if (codeType && typeof options.clientSecret === 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.MISSING_OAUTH_CLIENT_SECRET,
'The OAuth configuration client secret is required to enable OIDC code flow.',
);
}
}
}

/**
Expand Down Expand Up @@ -806,6 +882,20 @@ 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 we do not set responseType, we have idToken flow
// set to true by default.
if (typeof response.responseType !== 'undefined') {
this.responseType = response.responseType;
} else {
const responseType = {
idToken: true,
}
this.responseType = responseType;
}
}

/** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */
Expand All @@ -816,6 +906,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: 'The oauth response type object must set exact one response type 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
46 changes: 29 additions & 17 deletions test/integration/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1338,12 +1338,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 @@ -1637,13 +1646,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 @@ -1710,32 +1726,28 @@ describe('admin.auth', () => {
});
});

it('updateProviderConfig() successfully overwrites an OIDC config', () => {
const modifiedConfigOptions = {
it('updateProviderConfig() successfully partially modifies an OIDC config', () => {
const deltaChanges = {
displayName: 'OIDC_DISPLAY_NAME3',
enabled: false,
issuer: 'https://oidc.com/issuer3',
clientId: 'CLIENT_ID3',
};
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions)
.then((config) => {
const modifiedConfig = deepExtend(
{ providerId: authProviderConfig1.providerId }, modifiedConfigOptions);
assertDeepEqualUnordered(modifiedConfig, config);
});
});

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