Repository: firebase/firebase-admin-go
Pull request #468 (2021-11-15 22:17:44)

feat(auth): enables OIDC Auth code flow
Provides an option for developers to specify the OAuth response type for 
their OIDC provider (either one of these can be set:):

id_token
code (if set, must also set the client secret)

RELEASE NOTES: Added support for configuring the authorization code flow for OIDC providers.


Patch:
diff --git a/auth/provider_config.go b/auth/provider_config.go
index 17a941d..312fa34 100644
--- a/auth/provider_config.go
+++ b/auth/provider_config.go
@@ -38,11 +38,15 @@ const (
 	spEntityIDKey  = "spConfig.spEntityId"
 	callbackURIKey = "spConfig.callbackUri"
 
-	clientIDKey = "clientId"
-	issuerKey   = "issuer"
+	clientIDKey     = "clientId"
+	clientSecretKey = "clientSecret"
+	issuerKey       = "issuer"
 
 	displayNameKey = "displayName"
 	enabledKey     = "enabled"
+
+	idTokenResponseTypeKey = "responseType.idToken"
+	codeResponseTypeKey    = "responseType.code"
 )
 
 type nestedMap map[string]interface{}
@@ -113,11 +117,14 @@ func buildMask(data map[string]interface{}) []string {
 // OIDCProviderConfig is the OIDC auth provider configuration.
 // See https://openid.net/specs/openid-connect-core-1_0-final.html.
 type OIDCProviderConfig struct {
-	ID          string
-	DisplayName string
-	Enabled     bool
-	ClientID    string
-	Issuer      string
+	ID                  string
+	DisplayName         string
+	Enabled             bool
+	ClientID            string
+	Issuer              string
+	ClientSecret        string
+	CodeResponseType    bool
+	IDTokenResponseType bool
 }
 
 // OIDCProviderConfigToCreate represents the options used to create a new OIDCProviderConfig.
@@ -152,6 +159,27 @@ func (config *OIDCProviderConfigToCreate) Enabled(enabled bool) *OIDCProviderCon
 	return config.set(enabledKey, enabled)
 }
 
+// ClientSecret sets the client secret for the new provider.
+// This is required for the code flow.
+func (config *OIDCProviderConfigToCreate) ClientSecret(secret string) *OIDCProviderConfigToCreate {
+	return config.set(clientSecretKey, secret)
+}
+
+// IDTokenResponseType sets whether to enable the ID token response flow for the new provider.
+// By default, this is enabled if no response type is specified.
+// Having both the code and ID token response flows is currently not supported.
+func (config *OIDCProviderConfigToCreate) IDTokenResponseType(enabled bool) *OIDCProviderConfigToCreate {
+	return config.set(idTokenResponseTypeKey, enabled)
+}
+
+// CodeResponseType sets whether to enable the code response flow for the new provider.
+// By default, this is not enabled if no response type is specified.
+// A client secret must be set for this response type.
+// Having both the code and ID token response flows is currently not supported.
+func (config *OIDCProviderConfigToCreate) CodeResponseType(enabled bool) *OIDCProviderConfigToCreate {
+	return config.set(codeResponseTypeKey, enabled)
+}
+
 func (config *OIDCProviderConfigToCreate) set(key string, value interface{}) *OIDCProviderConfigToCreate {
 	if config.params == nil {
 		config.params = make(nestedMap)
@@ -180,6 +208,19 @@ func (config *OIDCProviderConfigToCreate) buildRequest() (nestedMap, string, err
 		return nil, "", fmt.Errorf("failed to parse Issuer: %v", err)
 	}
 
+	if val, ok := config.params.Get(codeResponseTypeKey); ok && val.(bool) {
+		if val, ok := config.params.GetString(clientSecretKey); !ok || val == "" {
+			return nil, "", errors.New("Client Secret must not be empty for Code Response Type")
+		}
+		if val, ok := config.params.Get(idTokenResponseTypeKey); ok && val.(bool) {
+			return nil, "", errors.New("Only one response type may be chosen")
+		}
+	} else if ok && !val.(bool) {
+		if val, ok := config.params.Get(idTokenResponseTypeKey); ok && !val.(bool) {
+			return nil, "", errors.New("At least one response type must be returned")
+		}
+	}
+
 	return config.params, config.id, nil
 }
 
@@ -213,6 +254,27 @@ func (config *OIDCProviderConfigToUpdate) Enabled(enabled bool) *OIDCProviderCon
 	return config.set(enabledKey, enabled)
 }
 
+// ClientSecret sets the client secret for the provider.
+// This is required for the code flow.
+func (config *OIDCProviderConfigToUpdate) ClientSecret(secret string) *OIDCProviderConfigToUpdate {
+	return config.set(clientSecretKey, secret)
+}
+
+// IDTokenResponseType sets whether to enable the ID token response flow for the provider.
+// By default, this is enabled if no response type is specified.
+// Having both the code and ID token response flows is currently not supported.
+func (config *OIDCProviderConfigToUpdate) IDTokenResponseType(enabled bool) *OIDCProviderConfigToUpdate {
+	return config.set(idTokenResponseTypeKey, enabled)
+}
+
+// CodeResponseType sets whether to enable the code response flow for the new provider.
+// By default, this is not enabled if no response type is specified.
+// A client secret must be set for this response type.
+// Having both the code and ID token response flows is currently not supported.
+func (config *OIDCProviderConfigToUpdate) CodeResponseType(enabled bool) *OIDCProviderConfigToUpdate {
+	return config.set(codeResponseTypeKey, enabled)
+}
+
 func (config *OIDCProviderConfigToUpdate) set(key string, value interface{}) *OIDCProviderConfigToUpdate {
 	if config.params == nil {
 		config.params = make(nestedMap)
@@ -240,6 +302,19 @@ func (config *OIDCProviderConfigToUpdate) buildRequest() (nestedMap, error) {
 		}
 	}
 
+	if val, ok := config.params.Get(codeResponseTypeKey); ok && val.(bool) {
+		if val, ok := config.params.GetString(clientSecretKey); !ok || val == "" {
+			return nil, errors.New("Client Secret must not be empty for Code Response Type")
+		}
+		if val, ok := config.params.Get(idTokenResponseTypeKey); ok && val.(bool) {
+			return nil, errors.New("Only one response type may be chosen")
+		}
+	} else if ok && !val.(bool) {
+		if val, ok := config.params.Get(idTokenResponseTypeKey); ok && !val.(bool) {
+			return nil, errors.New("At least one response type must be returned")
+		}
+	}
+
 	return config.params, nil
 }
 
@@ -826,20 +901,30 @@ func (c *baseClient) makeRequest(
 }
 
 type oidcProviderConfigDAO struct {
-	Name        string `json:"name"`
-	ClientID    string `json:"clientId"`
-	Issuer      string `json:"issuer"`
-	DisplayName string `json:"displayName"`
-	Enabled     bool   `json:"enabled"`
+	Name         string                   `json:"name"`
+	ClientID     string                   `json:"clientId"`
+	Issuer       string                   `json:"issuer"`
+	DisplayName  string                   `json:"displayName"`
+	Enabled      bool                     `json:"enabled"`
+	ClientSecret string                   `json:"clientSecret"`
+	ResponseType oidcProviderResponseType `json:"responseType"`
+}
+
+type oidcProviderResponseType struct {
+	Code    bool `json:"code"`
+	IDToken bool `json:"idToken"`
 }
 
 func (dao *oidcProviderConfigDAO) toOIDCProviderConfig() *OIDCProviderConfig {
 	return &OIDCProviderConfig{
-		ID:          extractResourceID(dao.Name),
-		DisplayName: dao.DisplayName,
-		Enabled:     dao.Enabled,
-		ClientID:    dao.ClientID,
-		Issuer:      dao.Issuer,
+		ID:                  extractResourceID(dao.Name),
+		DisplayName:         dao.DisplayName,
+		Enabled:             dao.Enabled,
+		ClientID:            dao.ClientID,
+		Issuer:              dao.Issuer,
+		ClientSecret:        dao.ClientSecret,
+		CodeResponseType:    dao.ResponseType.Code,
+		IDTokenResponseType: dao.ResponseType.IDToken,
 	}
 }
 


Test patch:
diff --git a/auth/provider_config_test.go b/auth/provider_config_test.go
index 7884375..0defec1 100644
--- a/auth/provider_config_test.go
+++ b/auth/provider_config_test.go
@@ -33,7 +33,12 @@ const oidcConfigResponse = `{
     "clientId": "CLIENT_ID",
     "issuer": "https://oidc.com/issuer",
     "displayName": "oidcProviderName",
-    "enabled": true
+    "enabled": true,
+		"clientSecret": "CLIENT_SECRET",
+		"responseType": {
+			"code": true,
+			"idToken": true
+		}
 }`
 
 const samlConfigResponse = `{
@@ -67,11 +72,14 @@ var idpCertsMap = []interface{}{
 }
 
 var oidcProviderConfig = &OIDCProviderConfig{
-	ID:          "oidc.provider",
-	DisplayName: "oidcProviderName",
-	Enabled:     true,
-	ClientID:    "CLIENT_ID",
-	Issuer:      "https://oidc.com/issuer",
+	ID:                  "oidc.provider",
+	DisplayName:         "oidcProviderName",
+	Enabled:             true,
+	ClientID:            "CLIENT_ID",
+	Issuer:              "https://oidc.com/issuer",
+	ClientSecret:        "CLIENT_SECRET",
+	CodeResponseType:    true,
+	IDTokenResponseType: true,
 }
 
 var samlProviderConfig = &SAMLProviderConfig{
@@ -157,7 +165,11 @@ func TestCreateOIDCProviderConfig(t *testing.T) {
 		DisplayName(oidcProviderConfig.DisplayName).
 		Enabled(oidcProviderConfig.Enabled).
 		ClientID(oidcProviderConfig.ClientID).
-		Issuer(oidcProviderConfig.Issuer)
+		Issuer(oidcProviderConfig.Issuer).
+		ClientSecret(oidcProviderConfig.ClientSecret).
+		CodeResponseType(true).
+		IDTokenResponseType(false)
+
 	oidc, err := client.CreateOIDCProviderConfig(context.Background(), options)
 	if err != nil {
 		t.Fatal(err)
@@ -168,10 +180,15 @@ func TestCreateOIDCProviderConfig(t *testing.T) {
 	}
 
 	wantBody := map[string]interface{}{
-		"displayName": oidcProviderConfig.DisplayName,
-		"enabled":     oidcProviderConfig.Enabled,
-		"clientId":    oidcProviderConfig.ClientID,
-		"issuer":      oidcProviderConfig.Issuer,
+		"displayName":  oidcProviderConfig.DisplayName,
+		"enabled":      oidcProviderConfig.Enabled,
+		"clientId":     oidcProviderConfig.ClientID,
+		"issuer":       oidcProviderConfig.Issuer,
+		"clientSecret": oidcProviderConfig.ClientSecret,
+		"responseType": map[string]interface{}{
+			"code":    true,
+			"idToken": false,
+		},
 	}
 	if err := checkCreateOIDCConfigRequest(s, wantBody); err != nil {
 		t.Fatal(err)
@@ -186,7 +203,8 @@ func TestCreateOIDCProviderConfigMinimal(t *testing.T) {
 	options := (&OIDCProviderConfigToCreate{}).
 		ID(oidcProviderConfig.ID).
 		ClientID(oidcProviderConfig.ClientID).
-		Issuer(oidcProviderConfig.Issuer)
+		Issuer(oidcProviderConfig.Issuer).
+		IDTokenResponseType(true)
 	oidc, err := client.CreateOIDCProviderConfig(context.Background(), options)
 	if err != nil {
 		t.Fatal(err)
@@ -197,8 +215,9 @@ func TestCreateOIDCProviderConfigMinimal(t *testing.T) {
 	}
 
 	wantBody := map[string]interface{}{
-		"clientId": oidcProviderConfig.ClientID,
-		"issuer":   oidcProviderConfig.Issuer,
+		"clientId":     oidcProviderConfig.ClientID,
+		"issuer":       oidcProviderConfig.Issuer,
+		"responseType": map[string]interface{}{"idToken": true},
 	}
 	if err := checkCreateOIDCConfigRequest(s, wantBody); err != nil {
 		t.Fatal(err)
@@ -215,7 +234,9 @@ func TestCreateOIDCProviderConfigZeroValues(t *testing.T) {
 		DisplayName("").
 		Enabled(false).
 		ClientID(oidcProviderConfig.ClientID).
-		Issuer(oidcProviderConfig.Issuer)
+		Issuer(oidcProviderConfig.Issuer).
+		CodeResponseType(false).
+		IDTokenResponseType(true)
 	oidc, err := client.CreateOIDCProviderConfig(context.Background(), options)
 	if err != nil {
 		t.Fatal(err)
@@ -230,6 +251,10 @@ func TestCreateOIDCProviderConfigZeroValues(t *testing.T) {
 		"enabled":     false,
 		"clientId":    oidcProviderConfig.ClientID,
 		"issuer":      oidcProviderConfig.Issuer,
+		"responseType": map[string]interface{}{
+			"code":    false,
+			"idToken": true,
+		},
 	}
 	if err := checkCreateOIDCConfigRequest(s, wantBody); err != nil {
 		t.Fatal(err)
@@ -246,7 +271,8 @@ func TestCreateOIDCProviderConfigError(t *testing.T) {
 	options := (&OIDCProviderConfigToCreate{}).
 		ID(oidcProviderConfig.ID).
 		ClientID(oidcProviderConfig.ClientID).
-		Issuer(oidcProviderConfig.Issuer)
+		Issuer(oidcProviderConfig.Issuer).
+		IDTokenResponseType(true)
 	oidc, err := client.CreateOIDCProviderConfig(context.Background(), options)
 	if oidc != nil || !errorutils.IsInternal(err) {
 		t.Errorf("CreateOIDCProviderConfig() = (%v, %v); want = (nil, %q)", oidc, err, "internal-error")
@@ -303,6 +329,36 @@ func TestCreateOIDCProviderConfigInvalidInput(t *testing.T) {
 				ClientID("CLIENT_ID").
 				Issuer("not a url"),
 		},
+		{
+			name: "MissingClientSecret",
+			want: "Client Secret must not be empty for Code Response Type",
+			conf: (&OIDCProviderConfigToCreate{}).
+				ID("oidc.provider").
+				ClientID("CLIENT_ID").
+				Issuer("https://oidc.com/issuer").
+				CodeResponseType(true),
+		},
+		{
+			name: "TwoResponseTypes",
+			want: "Only one response type may be chosen",
+			conf: (&OIDCProviderConfigToCreate{}).
+				ID("oidc.provider").
+				ClientID("CLIENT_ID").
+				Issuer("https://oidc.com/issuer").
+				IDTokenResponseType(true).
+				CodeResponseType(true).
+				ClientSecret("secret"),
+		},
+		{
+			name: "ZeroResponseTypes",
+			want: "At least one response type must be returned",
+			conf: (&OIDCProviderConfigToCreate{}).
+				ID("oidc.provider").
+				ClientID("CLIENT_ID").
+				Issuer("https://oidc.com/issuer").
+				IDTokenResponseType(false).
+				CodeResponseType(false),
+		},
 	}
 
 	client := &baseClient{}
@@ -323,7 +379,10 @@ func TestUpdateOIDCProviderConfig(t *testing.T) {
 		DisplayName(oidcProviderConfig.DisplayName).
 		Enabled(oidcProviderConfig.Enabled).
 		ClientID(oidcProviderConfig.ClientID).
-		Issuer(oidcProviderConfig.Issuer)
+		Issuer(oidcProviderConfig.Issuer).
+		ClientSecret(oidcProviderConfig.ClientSecret).
+		CodeResponseType(true).
+		IDTokenResponseType(false)
 	oidc, err := client.UpdateOIDCProviderConfig(context.Background(), "oidc.provider", options)
 	if err != nil {
 		t.Fatal(err)
@@ -334,16 +393,24 @@ func TestUpdateOIDCProviderConfig(t *testing.T) {
 	}
 
 	wantBody := map[string]interface{}{
-		"displayName": oidcProviderConfig.DisplayName,
-		"enabled":     oidcProviderConfig.Enabled,
-		"clientId":    oidcProviderConfig.ClientID,
-		"issuer":      oidcProviderConfig.Issuer,
+		"displayName":  oidcProviderConfig.DisplayName,
+		"enabled":      oidcProviderConfig.Enabled,
+		"clientId":     oidcProviderConfig.ClientID,
+		"issuer":       oidcProviderConfig.Issuer,
+		"clientSecret": oidcProviderConfig.ClientSecret,
+		"responseType": map[string]interface{}{
+			"code":    true,
+			"idToken": false,
+		},
 	}
 	wantMask := []string{
 		"clientId",
+		"clientSecret",
 		"displayName",
 		"enabled",
 		"issuer",
+		"responseType.code",
+		"responseType.idToken",
 	}
 	if err := checkUpdateOIDCConfigRequest(s, wantBody, wantMask); err != nil {
 		t.Fatal(err)
@@ -384,7 +451,9 @@ func TestUpdateOIDCProviderConfigZeroValues(t *testing.T) {
 	client := s.Client
 	options := (&OIDCProviderConfigToUpdate{}).
 		DisplayName("").
-		Enabled(false)
+		Enabled(false).
+		CodeResponseType(false).
+		IDTokenResponseType(true)
 	oidc, err := client.UpdateOIDCProviderConfig(context.Background(), "oidc.provider", options)
 	if err != nil {
 		t.Fatal(err)
@@ -397,10 +466,16 @@ func TestUpdateOIDCProviderConfigZeroValues(t *testing.T) {
 	wantBody := map[string]interface{}{
 		"displayName": nil,
 		"enabled":     false,
+		"responseType": map[string]interface{}{
+			"code":    false,
+			"idToken": true,
+		},
 	}
 	wantMask := []string{
 		"displayName",
 		"enabled",
+		"responseType.code",
+		"responseType.idToken",
 	}
 	if err := checkUpdateOIDCConfigRequest(s, wantBody, wantMask); err != nil {
 		t.Fatal(err)
@@ -455,6 +530,30 @@ func TestUpdateOIDCProviderConfigInvalidInput(t *testing.T) {
 			conf: (&OIDCProviderConfigToUpdate{}).
 				Issuer("not a url"),
 		},
+		{
+			name: "MissingClientSecret",
+			want: "Client Secret must not be empty for Code Response Type",
+			conf: (&OIDCProviderConfigToUpdate{}).
+				Issuer("https://oidc.com/issuer").
+				CodeResponseType(true),
+		},
+		{
+			name: "TwoResponseTypes",
+			want: "Only one response type may be chosen",
+			conf: (&OIDCProviderConfigToUpdate{}).
+				Issuer("https://oidc.com/issuer").
+				IDTokenResponseType(true).
+				CodeResponseType(true).
+				ClientSecret("secret"),
+		},
+		{
+			name: "ZeroResponseTypes",
+			want: "At least one response type must be returned",
+			conf: (&OIDCProviderConfigToUpdate{}).
+				Issuer("https://oidc.com/issuer").
+				IDTokenResponseType(false).
+				CodeResponseType(false),
+		},
 	}
 
 	client := &baseClient{}