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): Add TOTP support in Project and Tenant config #548

Merged
merged 5 commits into from
Mar 29, 2023
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
3 changes: 3 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ func NewClient(ctx context.Context, conf *internal.AuthConfig) (*Client, error)
userManagementEndpoint := idToolkitV1Endpoint
providerConfigEndpoint := idToolkitV2Endpoint
tenantMgtEndpoint := idToolkitV2Endpoint
projectMgtEndpoint := idToolkitV2Endpoint

base := &baseClient{
userManagementEndpoint: userManagementEndpoint,
providerConfigEndpoint: providerConfigEndpoint,
tenantMgtEndpoint: tenantMgtEndpoint,
projectMgtEndpoint: fmt.Sprintf("%s/projects/%s/config", projectMgtEndpoint, conf.ProjectID),
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
projectID: conf.ProjectID,
httpClient: hc,
idTokenVerifier: idTokenVerifier,
Expand Down Expand Up @@ -274,6 +276,7 @@ type baseClient struct {
userManagementEndpoint string
providerConfigEndpoint string
tenantMgtEndpoint string
projectMgtEndpoint string
projectID string
tenantID string
httpClient *internal.HTTPClient
Expand Down
93 changes: 93 additions & 0 deletions auth/multi_factor_config_mgt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import (
"fmt"
)

// ProviderConfig represents Multi Factor Provider configuration.
// Currently, only TOTP is supported.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
type ProviderConfig struct {

pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
// The state of multi-factor configuration, whether it's enabled or disabled.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
State MultiFactorConfigState `json:"state"`

// TOTPProviderConfig holds the TOTP (Time-based One-Time Password) configuration that is used in second factor authentication.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
TOTPProviderConfig *TOTPProviderConfig `json:"totpProviderConfig,omitempty"`
}

// TOTPProviderConfig represents configuration settings for TOTP second factor auth.
type TOTPProviderConfig struct {

// The number of adjacent intervals used by TOTP.
AdjacentIntervals int `json:"adjacentIntervals,omitempty"`
}

// MultiFactorConfigState represents whether the multi-factor configuration is enabled or disabled.
type MultiFactorConfigState string

// These constants represent the possible values for the MultiFactorConfigState type.
const (
Enabled MultiFactorConfigState = "ENABLED"
Disabled MultiFactorConfigState = "DISABLED"
)

// MultiFactorConfig represents a multi-factor configuration for Tenant/Project.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
// This can be used to define whether multi-factor authentication is enabled or disabled and the list of second factor challenges that are supported.
type MultiFactorConfig struct {

// A slice of pointers to ProviderConfig structs, each outlining the specific second factor authorization method.
ProviderConfigs []*ProviderConfig `json:"providerConfigs,omitempty"`
}

func (mfa *MultiFactorConfig) validate() error {
if mfa == nil {
return nil
}
if len(mfa.ProviderConfigs) == 0 {
return fmt.Errorf("\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s")
}
for _, providerConfig := range mfa.ProviderConfigs {
if providerConfig == nil {
return fmt.Errorf("\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s")
}
if err := providerConfig.validate(); err != nil {
return err
}
}
return nil
}

func (pvc *ProviderConfig) validate() error {
if pvc.State == "" && pvc.TOTPProviderConfig == nil {
return fmt.Errorf("\"ProviderConfig\" must be defined")
}
state := string(pvc.State)
if state != string(Enabled) && state != string(Disabled) {
lahirumaramba marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("\"ProviderConfig.State\" must be 'Enabled' or 'Disabled'")
}
return pvc.TOTPProviderConfig.validate()
}

func (tpvc *TOTPProviderConfig) validate() error {
if tpvc == nil {
return fmt.Errorf("\"TOTPProviderConfig\" must be defined")
}
if !(tpvc.AdjacentIntervals >= 1 && tpvc.AdjacentIntervals <= 10) {
return fmt.Errorf("\"AdjacentIntervals\" must be an integer between 1 and 10 (inclusive)")
}
return nil
}
110 changes: 110 additions & 0 deletions auth/multi_factor_config_mgt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2023 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import (
"testing"
)

func TestMultiFactorConfig(t *testing.T) {
mfa := MultiFactorConfig{
ProviderConfigs: []*ProviderConfig{{
State: Disabled,
TOTPProviderConfig: &TOTPProviderConfig{
AdjacentIntervals: 5,
},
}},
}
if err := mfa.validate(); err != nil {
t.Errorf("MultiFactorConfig not valid")
}
}
func TestMultiFactorConfigNoProviderConfigs(t *testing.T) {
mfa := MultiFactorConfig{}
want := "\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s"
if err := mfa.validate(); err.Error() != want {
t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want)
}
}

func TestMultiFactorConfigNilProviderConfigs(t *testing.T) {
mfa := MultiFactorConfig{
ProviderConfigs: nil,
}
want := "\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s"
if err := mfa.validate(); err.Error() != want {
t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want)
}
}

func TestMultiFactorConfigNilProviderConfig(t *testing.T) {
mfa := MultiFactorConfig{
ProviderConfigs: []*ProviderConfig{nil},
}
want := "\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s"
if err := mfa.validate(); err.Error() != want {
t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want)
}
}

func TestMultiFactorConfigUndefinedProviderConfig(t *testing.T) {
mfa := MultiFactorConfig{
ProviderConfigs: []*ProviderConfig{{}},
}
want := "\"ProviderConfig\" must be defined"
if err := mfa.validate(); err.Error() != want {
t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want)
}
}

func TestMultiFactorConfigInvalidProviderConfigState(t *testing.T) {
mfa := MultiFactorConfig{
ProviderConfigs: []*ProviderConfig{{
State: "invalid",
}},
}
want := "\"ProviderConfig.State\" must be 'Enabled' or 'Disabled'"
if err := mfa.validate(); err.Error() != want {
t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want)
}
}

func TestMultiFactorConfigNilTOTPProviderConfig(t *testing.T) {
mfa := MultiFactorConfig{
ProviderConfigs: []*ProviderConfig{{
State: Disabled,
TOTPProviderConfig: nil,
}},
}
want := "\"TOTPProviderConfig\" must be defined"
if err := mfa.validate(); err.Error() != want {
t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want)
}
}

func TestMultiFactorConfigInvalidAdjacentIntervals(t *testing.T) {
mfa := MultiFactorConfig{
ProviderConfigs: []*ProviderConfig{{
State: Disabled,
TOTPProviderConfig: &TOTPProviderConfig{
AdjacentIntervals: 11,
},
}},
}
want := "\"AdjacentIntervals\" must be an integer between 1 and 10 (inclusive)"
if err := mfa.validate(); err.Error() != want {
t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want)
}
}
113 changes: 113 additions & 0 deletions auth/project_config_mgt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2023 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import (
"context"
"errors"
"fmt"
"net/http"
"strings"

"firebase.google.com/go/v4/internal"
)

// ProjectConfig represents the properties to update on the provided project config.
type ProjectConfig struct {
MultiFactorConfig *MultiFactorConfig `json:"mfa,omitEmpty"`
}

func (base *baseClient) GetProjectConfig(ctx context.Context) (*ProjectConfig, error) {

req := &internal.Request{
Method: http.MethodGet,
URL: base.projectMgtEndpoint,
}
var result ProjectConfig
if _, err := base.httpClient.DoAndUnmarshal(ctx, req, &result); err != nil {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}
return &result, nil
}

func (base *baseClient) UpdateProjectConfig(ctx context.Context, projectConfig *ProjectConfigToUpdate) (*ProjectConfig, error) {
if projectConfig == nil {
return nil, errors.New("project config must not be nil")
}
if err := projectConfig.validate(); err != nil {
return nil, err
}
mask := projectConfig.params.UpdateMask()
if len(mask) == 0 {
return nil, errors.New("no parameters specified in the update request")
}
req := &internal.Request{
Method: http.MethodPatch,
URL: base.projectMgtEndpoint,
Body: internal.NewJSONEntity(projectConfig.params),
Opts: []internal.HTTPOption{
internal.WithQueryParam("updateMask", strings.Join(mask, ",")),
},
}
var result ProjectConfig
if _, err := base.httpClient.DoAndUnmarshal(ctx, req, &result); err != nil {
return nil, err
}
return &result, nil
}

// ProjectConfigToUpdate represents the options used to update the current project.
type ProjectConfigToUpdate struct {
params nestedMap
}

const (
multiFactorConfigProjectKey = "mfa"
)

// MultiFactorConfig configures the project's multi-factor settings
func (pc *ProjectConfigToUpdate) MultiFactorConfig(multiFactorConfig MultiFactorConfig) *ProjectConfigToUpdate {
return pc.set(multiFactorConfigProjectKey, multiFactorConfig)
}

func (pc *ProjectConfigToUpdate) set(key string, value interface{}) *ProjectConfigToUpdate {
pc.ensureParams().Set(key, value)
return pc
}

func (pc *ProjectConfigToUpdate) ensureParams() nestedMap {
if pc.params == nil {
pc.params = make(nestedMap)
}
return pc.params
}

func (pc *ProjectConfigToUpdate) validate() error {
req := make(map[string]interface{})
for k, v := range pc.params {
req[k] = v
}
val, ok := req[multiFactorConfigProjectKey]
if ok {
multiFactorConfig, ok := val.(MultiFactorConfig)
if !ok {
return fmt.Errorf("invalid type for MultiFactorConfig: %s", req[multiFactorConfigProjectKey])
}
if err := multiFactorConfig.validate(); err != nil {
return err
}
}
return nil
}
Loading