Skip to content

Commit 1d2ff69

Browse files
authored
feat(auth): Added code flow support for OIDC flow. (#1220)
* OIDC codeflow support * improve configs to simulate the real cases * update for changes in signiture * resolve comments * improve validator logic * remove unnecessary logic * add tests and fix errors * add auth-api-request rests
1 parent 7afaf6c commit 1d2ff69

File tree

7 files changed

+396
-17
lines changed

7 files changed

+396
-17
lines changed

etc/firebase-admin.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,15 +241,23 @@ export namespace auth {
241241
export interface MultiFactorUpdateSettings {
242242
enrolledFactors: UpdateMultiFactorInfoRequest[] | null;
243243
}
244+
export interface OAuthResponseType {
245+
code?: boolean;
246+
idToken?: boolean;
247+
}
244248
export interface OIDCAuthProviderConfig extends AuthProviderConfig {
245249
clientId: string;
250+
clientSecret?: string;
246251
issuer: string;
252+
responseType?: OAuthResponseType;
247253
}
248254
export interface OIDCUpdateAuthProviderRequest {
249255
clientId?: string;
256+
clientSecret?: string;
250257
displayName?: string;
251258
enabled?: boolean;
252259
issuer?: string;
260+
responseType?: OAuthResponseType;
253261
}
254262
export interface PhoneIdentifier {
255263
// (undocumented)

src/auth/auth-config.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import MultiFactorConfigState = auth.MultiFactorConfigState;
2424
import AuthFactorType = auth.AuthFactorType;
2525
import EmailSignInProviderConfig = auth.EmailSignInProviderConfig;
2626
import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig;
27+
import OAuthResponseType = auth.OAuthResponseType;
2728
import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig;
2829

2930
/** A maximum of 10 test phone number / code pairs can be configured. */
@@ -75,6 +76,8 @@ export interface OIDCConfigServerRequest {
7576
issuer?: string;
7677
displayName?: string;
7778
enabled?: boolean;
79+
clientSecret?: string;
80+
responseType?: OAuthResponseType;
7881
[key: string]: any;
7982
}
8083

@@ -87,6 +90,8 @@ export interface OIDCConfigServerResponse {
8790
issuer?: string;
8891
displayName?: string;
8992
enabled?: boolean;
93+
clientSecret?: string;
94+
responseType?: OAuthResponseType;
9095
}
9196

9297
/** The server side email configuration request interface. */
@@ -650,6 +655,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
650655
public readonly providerId: string;
651656
public readonly issuer: string;
652657
public readonly clientId: string;
658+
public readonly clientSecret?: string;
659+
public readonly responseType: OAuthResponseType;
653660

654661
/**
655662
* Converts a client side request to a OIDCConfigServerRequest which is the format
@@ -676,6 +683,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
676683
request.displayName = options.displayName;
677684
request.issuer = options.issuer;
678685
request.clientId = options.clientId;
686+
if (typeof options.clientSecret !== 'undefined') {
687+
request.clientSecret = options.clientSecret;
688+
}
689+
if (typeof options.responseType !== 'undefined') {
690+
request.responseType = options.responseType;
691+
}
679692
return request;
680693
}
681694

@@ -715,6 +728,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
715728
providerId: true,
716729
clientId: true,
717730
issuer: true,
731+
clientSecret: true,
732+
responseType: true,
733+
};
734+
const validResponseTypes = {
735+
idToken: true,
736+
code: true,
718737
};
719738
if (!validator.isNonNullObject(options)) {
720739
throw new FirebaseAuthError(
@@ -773,6 +792,59 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
773792
'"OIDCAuthProviderConfig.displayName" must be a valid string.',
774793
);
775794
}
795+
if (typeof options.clientSecret !== 'undefined' &&
796+
!validator.isNonEmptyString(options.clientSecret)) {
797+
throw new FirebaseAuthError(
798+
AuthClientErrorCode.INVALID_CONFIG,
799+
'"OIDCAuthProviderConfig.clientSecret" must be a valid string.',
800+
);
801+
}
802+
if (validator.isNonNullObject(options.responseType) && typeof options.responseType !== 'undefined') {
803+
Object.keys(options.responseType).forEach((key) => {
804+
if (!(key in validResponseTypes)) {
805+
throw new FirebaseAuthError(
806+
AuthClientErrorCode.INVALID_CONFIG,
807+
`"${key}" is not a valid OAuthResponseType parameter.`,
808+
);
809+
}
810+
});
811+
812+
const idToken = options.responseType.idToken;
813+
if (typeof idToken !== 'undefined' && !validator.isBoolean(idToken)) {
814+
throw new FirebaseAuthError(
815+
AuthClientErrorCode.INVALID_ARGUMENT,
816+
'"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.',
817+
);
818+
}
819+
820+
const code = options.responseType.code;
821+
if (typeof code !== 'undefined') {
822+
if (!validator.isBoolean(code)) {
823+
throw new FirebaseAuthError(
824+
AuthClientErrorCode.INVALID_ARGUMENT,
825+
'"OIDCAuthProviderConfig.responseType.code" must be a boolean.',
826+
);
827+
}
828+
829+
// If code flow is enabled, client secret must be provided.
830+
if (code && typeof options.clientSecret === 'undefined') {
831+
throw new FirebaseAuthError(
832+
AuthClientErrorCode.MISSING_OAUTH_CLIENT_SECRET,
833+
'The OAuth configuration client secret is required to enable OIDC code flow.',
834+
);
835+
}
836+
}
837+
838+
const allKeys = Object.keys(options.responseType).length;
839+
const enabledCount = Object.values(options.responseType).filter(Boolean).length;
840+
// Only one of OAuth response types can be set to true.
841+
if (allKeys > 1 && enabledCount != 1) {
842+
throw new FirebaseAuthError(
843+
AuthClientErrorCode.INVALID_OAUTH_RESPONSETYPE,
844+
'Only exactly one OAuth responseType should be set to true.',
845+
);
846+
}
847+
}
776848
}
777849

778850
/**
@@ -806,6 +878,13 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
806878
// When enabled is undefined, it takes its default value of false.
807879
this.enabled = !!response.enabled;
808880
this.displayName = response.displayName;
881+
882+
if (typeof response.clientSecret !== 'undefined') {
883+
this.clientSecret = response.clientSecret;
884+
}
885+
if (typeof response.responseType !== 'undefined') {
886+
this.responseType = response.responseType;
887+
}
809888
}
810889

811890
/** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */
@@ -816,6 +895,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
816895
providerId: this.providerId,
817896
issuer: this.issuer,
818897
clientId: this.clientId,
898+
clientSecret: deepCopy(this.clientSecret),
899+
responseType: deepCopy(this.responseType),
819900
};
820901
}
821902
}

src/auth/index.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,25 @@ export namespace auth {
12891289
callbackURL?: string;
12901290
}
12911291

1292+
/**
1293+
* The interface representing OIDC provider's response object for OAuth
1294+
* authorization flow.
1295+
* We need either of them to be true, there are two cases:
1296+
* If set code to true, then we are doing code flow.
1297+
* If set idToken to true, then we are doing idToken flow.
1298+
*/
1299+
export interface OAuthResponseType {
1300+
/**
1301+
* Whether ID token is returned from IdP's authorization endpoint.
1302+
*/
1303+
idToken?: boolean;
1304+
1305+
/**
1306+
* Whether authorization code is returned from IdP's authorization endpoint.
1307+
*/
1308+
code?: boolean;
1309+
}
1310+
12921311
/**
12931312
* The [OIDC](https://openid.net/specs/openid-connect-core-1_0-final.html) Auth
12941313
* provider configuration interface. An OIDC provider can be created via
@@ -1321,6 +1340,16 @@ export namespace auth {
13211340
* [spec](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).
13221341
*/
13231342
issuer: string;
1343+
1344+
/**
1345+
* The OIDC provider's client secret to enable OIDC code flow.
1346+
*/
1347+
clientSecret?: string;
1348+
1349+
/**
1350+
* The OIDC provider's response object for OAuth authorization flow.
1351+
*/
1352+
responseType?: OAuthResponseType;
13241353
}
13251354

13261355
/**
@@ -1403,6 +1432,17 @@ export namespace auth {
14031432
* configuration's value is not modified.
14041433
*/
14051434
issuer?: string;
1435+
1436+
/**
1437+
* The OIDC provider's client secret to enable OIDC code flow.
1438+
* If not provided, the existing configuration's value is not modified.
1439+
*/
1440+
clientSecret?: string;
1441+
1442+
/**
1443+
* The OIDC provider's response object for OAuth authorization flow.
1444+
*/
1445+
responseType?: OAuthResponseType;
14061446
}
14071447

14081448
/**

src/utils/error.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,10 @@ export class AuthClientErrorCode {
525525
code: 'invalid-provider-uid',
526526
message: 'The providerUid must be a valid provider uid string.',
527527
};
528+
public static INVALID_OAUTH_RESPONSETYPE = {
529+
code: 'invalid-oauth-responsetype',
530+
message: 'Only exactly one OAuth responseType should be set to true.',
531+
};
528532
public static INVALID_SESSION_COOKIE_DURATION = {
529533
code: 'invalid-session-cookie-duration',
530534
message: 'The session cookie duration must be a valid number in milliseconds ' +
@@ -597,6 +601,10 @@ export class AuthClientErrorCode {
597601
code: 'missing-oauth-client-id',
598602
message: 'The OAuth/OIDC configuration client ID must not be empty.',
599603
};
604+
public static MISSING_OAUTH_CLIENT_SECRET = {
605+
code: 'missing-oauth-client-secret',
606+
message: 'The OAuth configuration client secret is required to enable OIDC code flow.',
607+
};
600608
public static MISSING_PROVIDER_ID = {
601609
code: 'missing-provider-id',
602610
message: 'A valid provider ID must be provided in the request.',

test/integration/auth.spec.ts

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,12 +1334,21 @@ describe('admin.auth', () => {
13341334
enabled: true,
13351335
issuer: 'https://oidc.com/issuer1',
13361336
clientId: 'CLIENT_ID1',
1337+
responseType: {
1338+
idToken: true,
1339+
code: false,
1340+
},
13371341
};
13381342
const modifiedConfigOptions = {
13391343
displayName: 'OIDC_DISPLAY_NAME3',
13401344
enabled: false,
13411345
issuer: 'https://oidc.com/issuer3',
13421346
clientId: 'CLIENT_ID3',
1347+
clientSecret: 'CLIENT_SECRET',
1348+
responseType: {
1349+
idToken: false,
1350+
code: true,
1351+
},
13431352
};
13441353

13451354
before(function() {
@@ -1633,13 +1642,20 @@ describe('admin.auth', () => {
16331642
enabled: true,
16341643
issuer: 'https://oidc.com/issuer1',
16351644
clientId: 'CLIENT_ID1',
1645+
responseType: {
1646+
idToken: true,
1647+
},
16361648
};
16371649
const authProviderConfig2 = {
16381650
providerId: randomOidcProviderId(),
16391651
displayName: 'OIDC_DISPLAY_NAME2',
16401652
enabled: true,
16411653
issuer: 'https://oidc.com/issuer2',
16421654
clientId: 'CLIENT_ID2',
1655+
clientSecret: 'CLIENT_SECRET',
1656+
responseType: {
1657+
code: true,
1658+
},
16431659
};
16441660

16451661
const removeTempConfigs = (): Promise<any> => {
@@ -1706,39 +1722,65 @@ describe('admin.auth', () => {
17061722
});
17071723
});
17081724

1709-
it('updateProviderConfig() successfully overwrites an OIDC config', () => {
1725+
it('updateProviderConfig() successfully partially modifies an OIDC config', () => {
1726+
const deltaChanges = {
1727+
displayName: 'OIDC_DISPLAY_NAME3',
1728+
enabled: false,
1729+
issuer: 'https://oidc.com/issuer3',
1730+
clientId: 'CLIENT_ID3',
1731+
clientSecret: 'CLIENT_SECRET',
1732+
responseType: {
1733+
idToken: false,
1734+
code: true,
1735+
},
1736+
};
1737+
// Only above fields should be modified.
17101738
const modifiedConfigOptions = {
1739+
providerId: authProviderConfig1.providerId,
17111740
displayName: 'OIDC_DISPLAY_NAME3',
17121741
enabled: false,
17131742
issuer: 'https://oidc.com/issuer3',
17141743
clientId: 'CLIENT_ID3',
1744+
clientSecret: 'CLIENT_SECRET',
1745+
responseType: {
1746+
code: true,
1747+
},
17151748
};
1716-
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions)
1749+
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges)
17171750
.then((config) => {
1718-
const modifiedConfig = deepExtend(
1719-
{ providerId: authProviderConfig1.providerId }, modifiedConfigOptions);
1720-
assertDeepEqualUnordered(modifiedConfig, config);
1751+
assertDeepEqualUnordered(modifiedConfigOptions, config);
17211752
});
17221753
});
17231754

1724-
it('updateProviderConfig() successfully partially modifies an OIDC config', () => {
1755+
it('updateProviderConfig() with invalid oauth response type should be rejected', () => {
17251756
const deltaChanges = {
17261757
displayName: 'OIDC_DISPLAY_NAME4',
1758+
enabled: false,
17271759
issuer: 'https://oidc.com/issuer4',
1760+
clientId: 'CLIENT_ID4',
1761+
clientSecret: 'CLIENT_SECRET',
1762+
responseType: {
1763+
idToken: false,
1764+
code: false,
1765+
},
17281766
};
1729-
// Only above fields should be modified.
1730-
const modifiedConfigOptions = {
1731-
displayName: 'OIDC_DISPLAY_NAME4',
1767+
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges).
1768+
should.eventually.be.rejected.and.have.property('code', 'auth/invalid-oauth-responsetype');
1769+
});
1770+
1771+
it('updateProviderConfig() code flow with no client secret should be rejected', () => {
1772+
const deltaChanges = {
1773+
displayName: 'OIDC_DISPLAY_NAME5',
17321774
enabled: false,
1733-
issuer: 'https://oidc.com/issuer4',
1734-
clientId: 'CLIENT_ID3',
1775+
issuer: 'https://oidc.com/issuer5',
1776+
clientId: 'CLIENT_ID5',
1777+
responseType: {
1778+
idToken: false,
1779+
code: true,
1780+
},
17351781
};
1736-
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges)
1737-
.then((config) => {
1738-
const modifiedConfig = deepExtend(
1739-
{ providerId: authProviderConfig1.providerId }, modifiedConfigOptions);
1740-
assertDeepEqualUnordered(modifiedConfig, config);
1741-
});
1782+
return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges).
1783+
should.eventually.be.rejected.and.have.property('code', 'auth/missing-oauth-client-secret');
17421784
});
17431785

17441786
it('deleteProviderConfig() successfully deletes an existing OIDC config', () => {

0 commit comments

Comments
 (0)