diff --git a/go.mod b/go.mod index 41685e1..3795e3d 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,8 @@ require ( github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect github.com/Nerzal/gocloak/v13 v13.8.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/coreos/go-oidc/v3 v3.6.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-resty/resty/v2 v2.7.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index fc7e258..ce244d0 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/confluentinc/confluent-kafka-go v1.4.2 h1:13EK9RTujF7lVkvHQ5Hbu6bM+Yfrq8L0MkJNnjHSd4Q= github.com/confluentinc/confluent-kafka-go v1.4.2/go.mod h1:u2zNLny2xq+5rWeTQjFHbDzzNuba4P1vo31r9r4uAdg= +github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o= +github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -57,6 +59,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= @@ -180,6 +184,7 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/src/config/constants.go b/src/config/constants.go index 0169c16..b94ef83 100644 --- a/src/config/constants.go +++ b/src/config/constants.go @@ -25,6 +25,7 @@ const ( DataAgreementRecordId = "dataAgreementRecordId" IndividualId = "individualId" DeliveryId = "deliveryId" + IdpId = "idpId" ) // Schemas diff --git a/src/database/db.go b/src/database/db.go index bf3c749..6ad7301 100644 --- a/src/database/db.go +++ b/src/database/db.go @@ -147,6 +147,11 @@ func Init(config *config.Configuration) error { return err } + err = initCollection("identityProviders", []string{"id"}, true) + if err != nil { + return err + } + return nil } diff --git a/src/main/main.go b/src/main/main.go index b4552d3..66d72e4 100644 --- a/src/main/main.go +++ b/src/main/main.go @@ -15,9 +15,9 @@ import ( "github.com/bb-consent/api/src/token" v1Handlers "github.com/bb-consent/api/src/v1/handler" v1HttpPaths "github.com/bb-consent/api/src/v1/http_path" - v2Handlers "github.com/bb-consent/api/src/v2/handler" - individualHandler "github.com/bb-consent/api/src/v2/handler/individual" v2HttpPaths "github.com/bb-consent/api/src/v2/http_path" + "github.com/bb-consent/api/src/v2/iam" + "github.com/bb-consent/api/src/v2/twilio" "github.com/bb-consent/api/src/webhooks" "github.com/casbin/casbin/v2" "github.com/gorilla/mux" @@ -51,10 +51,13 @@ func startAPICmdHandlerfunc(cmd *cobra.Command, args []string) { // IAM v1Handlers.IamInit(loadedConfig) - v2Handlers.IamInit(loadedConfig) - individualHandler.IamInit(loadedConfig) + iam.Init(loadedConfig) log.Println("Iam initialized") + // Twilio + twilio.Init(loadedConfig) + log.Println("Twilio initialized") + // Email email.Init(loadedConfig) log.Println("Email initialized") diff --git a/src/v2/error_handler/error_handler.go b/src/v2/error_handler/error_handler.go new file mode 100644 index 0000000..857ddcf --- /dev/null +++ b/src/v2/error_handler/error_handler.go @@ -0,0 +1,29 @@ +package error_handler + +import ( + "encoding/json" + "net/http" +) + +type HttpError struct { + Status int `json:"errorCode"` + Message string `json:"errorDescription"` +} + +func HandleExit(w http.ResponseWriter) { + r := recover() + if r != nil { + if he, ok := r.(HttpError); ok { + response, _ := json.Marshal(he) + w.WriteHeader(he.Status) + w.Header().Set("Content-Type", "application/json") + w.Write(response) + } else { + panic(r) + } + } +} + +func Exit(status int, message string) { + panic(HttpError{Status: status, Message: message}) +} diff --git a/src/v2/handler/addidentityprovider_handler.go b/src/v2/handler/addidentityprovider_handler.go deleted file mode 100644 index 14aebd6..0000000 --- a/src/v2/handler/addidentityprovider_handler.go +++ /dev/null @@ -1,405 +0,0 @@ -package handler - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "strings" - "time" - - "github.com/asaskevich/govalidator" - "github.com/bb-consent/api/src/common" - "github.com/bb-consent/api/src/config" - "github.com/bb-consent/api/src/org" -) - -var timeout time.Duration - -var iamConfig config.Iam -var twilioConfig config.Twilio - -// IamInit Initialize the IAM handler -func IamInit(config *config.Configuration) { - iamConfig = config.Iam - twilioConfig = config.Twilio - timeout = time.Duration(time.Duration(iamConfig.Timeout) * time.Second) - - /* - memStorage := storage.NewMemoryStorage() - s := scheduler.New(memStorage) - _, err := s.RunEvery(24*time.Hour, clearStaleOtps) - if err != nil { - log.Printf("err in scheduling clearStaleOtps: %v", err) - } - - //TODO: Enable this later phase - //s.Start() - */ -} - -// IdentityProviderReq Describes the request payload to create and update an external identity provider -type IdentityProviderReq struct { - AuthorizationURL string `json:"authorizationUrl" valid:"required"` - TokenURL string `json:"tokenUrl" valid:"required"` - LogoutURL string `json:"logoutUrl"` - ClientID string `json:"clientId" valid:"required"` - ClientSecret string `json:"clientSecret" valid:"required"` - JWKSURL string `json:"jwksUrl"` - UserInfoURL string `json:"userInfoUrl"` - ValidateSignature bool `json:"validateSignature"` - DisableUserInfo bool `json:"disableUserInfo"` - Issuer string `json:"issuer"` - DefaultScope string `json:"defaultScope"` -} - -type iamToken struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - RefreshExpiresIn int `json:"refresh_expires_in"` - RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type"` -} - -type iamError struct { - ErrorType string `json:"error"` - Error string `json:"error_description"` -} - -func getAdminToken() (iamToken, int, iamError, error) { - t, status, iamErr, err := getToken(iamConfig.AdminUser, iamConfig.AdminPassword, "admin-cli", "master") - return t, status, iamErr, err -} - -func getToken(username string, password string, clientID string, realm string) (iamToken, int, iamError, error) { - var tok iamToken - var e iamError - var status = http.StatusInternalServerError - - data := url.Values{} - data.Set("username", username) - data.Add("password", password) - data.Add("client_id", clientID) - data.Add("grant_type", "password") - - resp, err := http.PostForm(iamConfig.URL+"/realms/"+realm+"/protocol/openid-connect/token", data) - if err != nil { - return tok, status, e, err - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - if err != nil { - return tok, status, e, err - } - if resp.StatusCode != http.StatusOK { - var e iamError - json.Unmarshal(body, &e) - return tok, resp.StatusCode, e, errors.New("failed to get token") - } - json.Unmarshal(body, &tok) - - return tok, resp.StatusCode, e, err -} - -// Helper function to add identity provider to iGrant.io IAM -func addIdentityProvider(identityProviderRepresentation org.IdentityProviderRepresentation, adminToken string) (int, iamError, error) { - var e iamError - var status = http.StatusInternalServerError - jsonReq, _ := json.Marshal(identityProviderRepresentation) - req, err := http.NewRequest("POST", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/identity-provider/instances", bytes.NewBuffer(jsonReq)) - if err != nil { - return status, e, err - } - - req.Header.Add("Authorization", "Bearer "+adminToken) - req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) - - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Do(req) - if err != nil { - return status, e, err - } - - if resp.StatusCode != http.StatusCreated { - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - type errorMsg struct { - ErrorMessage string `json:"errorMessage"` - } - var errMsg errorMsg - json.Unmarshal(body, &errMsg) - e.Error = errMsg.ErrorMessage - e.ErrorType = "Identity provider creation failed" - return resp.StatusCode, e, errors.New("Failed to create identity provider") - } - - defer resp.Body.Close() - - return resp.StatusCode, e, err -} - -// Helper function to add OpenID client to manage login sessions for the external identity provider -func addOpenIDClient(keycloakOpenIDClient org.KeycloakOpenIDClient, adminToken string) (int, iamError, error) { - - var e iamError - var status = http.StatusInternalServerError - jsonReq, _ := json.Marshal(keycloakOpenIDClient) - req, err := http.NewRequest("POST", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/clients", bytes.NewBuffer(jsonReq)) - if err != nil { - return status, e, err - } - - req.Header.Add("Authorization", "Bearer "+adminToken) - req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) - - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Do(req) - if err != nil { - return status, e, err - } - - if resp.StatusCode != http.StatusCreated { - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - type errorMsg struct { - ErrorMessage string `json:"errorMessage"` - } - var errMsg errorMsg - json.Unmarshal(body, &errMsg) - e.Error = errMsg.ErrorMessage - e.ErrorType = "OpenID client creation failed" - return resp.StatusCode, e, errors.New("Failed to create OpenID client") - } - - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - var responseBody interface{} - json.Unmarshal(body, &responseBody) - - return resp.StatusCode, e, err - -} - -// AddIdentityProvider Add an external identity provider for an organization -func AddIdentityProvider(w http.ResponseWriter, r *http.Request) { - - // Note: Set OpenID-Connect as subscription method for the organization - // Execute set subscription method API to do the same. - - // Get the org ID and fetch the organization from the db. - organizationID := r.Header.Get(config.OrganizationId) - o, err := org.Get(organizationID) - - if err != nil { - m := fmt.Sprintf("Failed to fetch org; Failed to create identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - if o.ExternalIdentityProviderAvailable { - m := fmt.Sprintf("External IDP exists; Try to update instead of create; Failed to create identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - // Deserializing the request payload to struct - b, _ := ioutil.ReadAll(r.Body) - defer r.Body.Close() - - var addIdentityProviderReq IdentityProviderReq - json.Unmarshal(b, &addIdentityProviderReq) - - // validating request payload - valid, err := govalidator.ValidateStruct(addIdentityProviderReq) - if valid != true { - m := fmt.Sprintf("Missing mandatory params for creating identity provider for org:%v\n", organizationID) - common.HandleError(w, http.StatusBadRequest, m, err) - return - } - - var identityProviderOpenIDConfig org.IdentityProviderOpenIDConfig - - // OpenID config - identityProviderOpenIDConfig.AuthorizationURL = addIdentityProviderReq.AuthorizationURL - identityProviderOpenIDConfig.TokenURL = addIdentityProviderReq.TokenURL - identityProviderOpenIDConfig.LogoutURL = addIdentityProviderReq.LogoutURL - identityProviderOpenIDConfig.ClientID = addIdentityProviderReq.ClientID - identityProviderOpenIDConfig.ClientSecret = addIdentityProviderReq.ClientSecret - identityProviderOpenIDConfig.JWKSURL = addIdentityProviderReq.JWKSURL - identityProviderOpenIDConfig.UserInfoURL = addIdentityProviderReq.UserInfoURL - identityProviderOpenIDConfig.ValidateSignature = addIdentityProviderReq.ValidateSignature - identityProviderOpenIDConfig.DefaultScope = addIdentityProviderReq.DefaultScope - - if len(strings.TrimSpace(addIdentityProviderReq.LogoutURL)) > 0 { - identityProviderOpenIDConfig.BackchannelSupported = true - } else { - identityProviderOpenIDConfig.BackchannelSupported = false - } - - identityProviderOpenIDConfig.DisableUserInfo = addIdentityProviderReq.DisableUserInfo - identityProviderOpenIDConfig.Issuer = addIdentityProviderReq.Issuer - - if len(strings.TrimSpace(addIdentityProviderReq.JWKSURL)) > 0 { - identityProviderOpenIDConfig.UseJWKSURL = true - } else { - identityProviderOpenIDConfig.UseJWKSURL = false - } - - identityProviderOpenIDConfig.SyncMode = "IMPORT" - identityProviderOpenIDConfig.ClientAuthMethod = "client_secret_post" - identityProviderOpenIDConfig.HideOnLoginPage = true - - // Fetch admin token from keycloak - t, status, _, err := getAdminToken() - if err != nil { - m := fmt.Sprintf("Failed to fetch the admin token from keycloak; Failed to create identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Constructing the request payload for creating identity provider - var identityProviderRepresentation = org.IdentityProviderRepresentation{ - Config: identityProviderOpenIDConfig, - Alias: o.ID.Hex(), - ProviderID: "oidc", - AuthenticateByDefault: false, - Enabled: true, - FirstBrokerLoginFlowAlias: iamConfig.ExternalIdentityProvidersConfiguration.IdentityProviderCustomerAutoLinkFlowName, - LinkOnly: false, - AddReadTokenRoleOnCreate: false, - PostBrokerLoginFlowAlias: "", - StoreToken: false, - TrustEmail: false, - } - - // Add identity provider to iGrant.io IAM - status, _, err = addIdentityProvider(identityProviderRepresentation, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to create external identity provider in keycloak; Failed to create identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Save the identity provider details to the database - o, err = org.UpdateIdentityProviderByOrgID(organizationID, identityProviderRepresentation) - if err != nil { - m := fmt.Sprintf("Failed to update IDP config to database; Failed to create identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Update external identity provider available status - o, err = org.UpdateExternalIdentityProviderAvailableStatus(organizationID, true) - if err != nil { - m := fmt.Sprintf("Failed to update external identity provider available status; Failed to create identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // FIX ME : Is this right practice to do it anonymous function executed in a separate thread ? - go func() { - // Create a client to manage the user session from external identity provider - - // ID for a custom authentication flow created in the keycloak to manage the conversion of external access token to internal (iGrant.io) authorization code - var IDPCustomKeycloakAuthenticationFlow = iamConfig.ExternalIdentityProvidersConfiguration.IdentityProviderCustomerAuthenticationFlowID - - // Construct the request payload to create OpenID client - var keycloakOpenIDClient = org.KeycloakOpenIDClient{ - Access: org.KeycloakOpenIDClientAccess{ - Configure: true, - View: true, - Manage: true, - }, - AlwaysDisplayInConsole: false, - Attributes: org.KeycloakOpenIDClientAttributes{ - BackchannelLogoutRevokeOfflineTokens: "true", - BackchannelLogoutSessionRequired: "true", - BackchannelLogoutURL: identityProviderOpenIDConfig.LogoutURL, - ClientCredentialsUseRefreshToken: "false", - DisplayOnConsentScreen: "false", - ExcludeSessionStateFromAuthResponse: "false", - SamlAssertionSignature: "false", - SamlAuthnstatement: "false", - SamlClientSignature: "false", - SamlEncrypt: "false", - SamlForcePostBinding: "false", - SamlMultivaluedRoles: "false", - SamlOnetimeuseCondition: "false", - SamlServerSignature: "false", - SamlServerSignatureKeyinfoExt: "false", - SamlForceNameIDFormat: "false", - TLSClientCertificateBoundAccessTokens: "false", - }, - AuthenticationFlowBindingOverrides: org.KeycloakOpenIDClientAuthenticationFlowBindingOverrides{ - Browser: IDPCustomKeycloakAuthenticationFlow, - DirectGrant: IDPCustomKeycloakAuthenticationFlow, - }, - BearerOnly: false, - ClientAuthenticatorType: "client-secret", - ClientID: o.ID.Hex(), - ConsentRequired: false, - DefaultClientScopes: []string{ - "web-origins", - "role_list", - "profile", - "roles", - "email", - }, - DirectAccessGrantsEnabled: true, - Enabled: true, - FrontchannelLogout: false, - FullScopeAllowed: true, - ImplicitFlowEnabled: false, - NodeReRegistrationTimeout: -1, - NotBefore: 0, - OptionalClientScopes: []string{ - "address", - "phone", - "offline_access", - "microprofile-jwt", - }, - Protocol: "openid-connect", - PublicClient: false, - RedirectUris: []string{}, - ServiceAccountsEnabled: false, - StandardFlowEnabled: true, - SurrogateAuthRequired: false, - WebOrigins: []string{}, - } - - // Add OpenID client to iGrant.io IAM - status, _, err = addOpenIDClient(keycloakOpenIDClient, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to create OpenID client in keycloak; Failed to create identity provider for %v", organizationID) - log.Println(m) - return - } - - // Save the OpenID client details to the database - o, err = org.UpdateOpenIDClientByOrgID(organizationID, keycloakOpenIDClient) - if err != nil { - m := fmt.Sprintf("Failed to update OpenID client config to database; Failed to create identity provider for %v", organizationID) - log.Println(m) - return - } - - }() - - response, _ := json.Marshal(o.IdentityProviderRepresentation.Config) - w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) - w.WriteHeader(http.StatusCreated) - w.Write(response) - -} diff --git a/src/v2/handler/deleteidentityprovider_handler.go b/src/v2/handler/deleteidentityprovider_handler.go deleted file mode 100644 index e646376..0000000 --- a/src/v2/handler/deleteidentityprovider_handler.go +++ /dev/null @@ -1,181 +0,0 @@ -package handler - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "log" - "net/http" - - "github.com/bb-consent/api/src/common" - "github.com/bb-consent/api/src/config" - "github.com/bb-consent/api/src/org" -) - -// Helper function to delete identity provider to iGrant.io IAM -func deleteIdentityProvider(identityProviderAlias string, adminToken string) (int, iamError, error) { - var e iamError - var status = http.StatusInternalServerError - req, err := http.NewRequest("DELETE", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/identity-provider/instances/"+identityProviderAlias, nil) - if err != nil { - return status, e, err - } - - req.Header.Add("Authorization", "Bearer "+adminToken) - req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) - - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Do(req) - if err != nil { - return status, e, err - } - - if resp.StatusCode != http.StatusNoContent { - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - type errorMsg struct { - ErrorMessage string `json:"errorMessage"` - } - var errMsg errorMsg - json.Unmarshal(body, &errMsg) - e.Error = errMsg.ErrorMessage - e.ErrorType = "Identity provider delete failed" - return resp.StatusCode, e, errors.New("failed to delete identity provider") - } - - defer resp.Body.Close() - - return resp.StatusCode, e, err -} - -// Helper function to delete OpenID client to manage login sessions for the external identity provider -func deleteOpenIDClient(clientUUID string, adminToken string) (int, iamError, error) { - - var e iamError - var status = http.StatusInternalServerError - req, err := http.NewRequest("DELETE", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/clients/"+clientUUID, nil) - if err != nil { - return status, e, err - } - - req.Header.Add("Authorization", "Bearer "+adminToken) - req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) - - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Do(req) - if err != nil { - return status, e, err - } - - if resp.StatusCode != http.StatusNoContent { - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - type errorMsg struct { - ErrorMessage string `json:"errorMessage"` - } - var errMsg errorMsg - json.Unmarshal(body, &errMsg) - e.Error = errMsg.ErrorMessage - e.ErrorType = "OpenID client delete failed" - return resp.StatusCode, e, errors.New("failed to delete OpenID client") - } - - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - var responseBody interface{} - json.Unmarshal(body, &responseBody) - - return resp.StatusCode, e, err - -} - -// DeleteIdentityProvider Delete external identity provider for an organisation -func DeleteIdentityProvider(w http.ResponseWriter, r *http.Request) { - - // Get the org ID and fetch the organization from the db. - organizationID := r.Header.Get(config.OrganizationId) - o, err := org.Get(organizationID) - - if err != nil { - m := fmt.Sprintf("Failed to fetch org; Failed to delete identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - if !o.ExternalIdentityProviderAvailable { - m := fmt.Sprintf("External IDP provider doesn't exist; Try to create instead of delete; Failed to delete identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - // Fetch admin token from keycloak - t, status, _, err := getAdminToken() - if err != nil { - m := fmt.Sprintf("Failed to fetch the admin token from keycloak; Failed to delete identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Delete identity provider in IAM - status, _, err = deleteIdentityProvider(o.IdentityProviderRepresentation.Alias, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to delete external identity provider in keycloak; Failed to delete identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Update external identity provider available status - o, err = org.UpdateExternalIdentityProviderAvailableStatus(organizationID, false) - if err != nil { - m := fmt.Sprintf("Failed to update external identity provider available status; Failed to delete identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Delete the identity provider details from the database - o, err = org.DeleteIdentityProviderByOrgID(organizationID) - if err != nil { - m := fmt.Sprintf("Failed to delete IDP config to database; Failed to delete identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // FIX ME : Is this right practice to do it anonymous function executed in a separate thread ? - go func() { - - // Fetch OpenID client UUID from IAM - openIDClientUUID, _, _, err := getClientsInRealm(o.KeycloakOpenIDClient.ClientID, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to fetch OpenID client UUID from keycloak; Failed to delete identity provider for %v", organizationID) - log.Println(m) - return - } - - // Delete OpenID client in iGrant.io IAM - _, _, err = deleteOpenIDClient(openIDClientUUID, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to delete external identity provider in keycloak; Failed to delete identity provider for %v", organizationID) - log.Println(m) - return - } - - // Delete the OpenID client details from the database - _, err = org.DeleteOpenIDClientByOrgID(organizationID) - if err != nil { - m := fmt.Sprintf("Failed to delete OpenID config to database; Failed to delete identity provider for %v", organizationID) - log.Println(m) - return - } - - }() - - w.WriteHeader(http.StatusNoContent) - w.Write(nil) -} diff --git a/src/v2/handler/getidentityprovider_handler.go b/src/v2/handler/getidentityprovider_handler.go deleted file mode 100644 index fd2fc9d..0000000 --- a/src/v2/handler/getidentityprovider_handler.go +++ /dev/null @@ -1,36 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/bb-consent/api/src/common" - "github.com/bb-consent/api/src/config" - "github.com/bb-consent/api/src/org" -) - -// GetIdentityProvider Get external identity provider for an organisation -func GetIdentityProvider(w http.ResponseWriter, r *http.Request) { - - // Get the org ID and fetch the organization from the db. - organizationID := r.Header.Get(config.OrganizationId) - o, err := org.Get(organizationID) - - if err != nil { - m := fmt.Sprintf("Failed to fetch org; Failed to get identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - if !o.ExternalIdentityProviderAvailable { - m := fmt.Sprintf("External IDP provider doesn't exist; Try to create instead of get; Failed to get identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - response, _ := json.Marshal(o.IdentityProviderRepresentation.Config) - w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) - w.WriteHeader(http.StatusOK) - w.Write(response) -} diff --git a/src/v2/handler/idp/config_create_idp.go b/src/v2/handler/idp/config_create_idp.go new file mode 100644 index 0000000..bd8a938 --- /dev/null +++ b/src/v2/handler/idp/config_create_idp.go @@ -0,0 +1,85 @@ +package idp + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/idp" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type addIdpReq struct { + Idp idp.IdentityProvider `json:"idp" valid:"required"` +} + +type addIdpResp struct { + Idp idp.IdentityProvider `json:"idp" valid:"required"` +} + +func ConfigCreateIdp(w http.ResponseWriter, r *http.Request) { + + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + // Request body + var idpReq addIdpReq + b, _ := io.ReadAll(r.Body) + defer r.Body.Close() + json.Unmarshal(b, &idpReq) + + // Repository + idpRepo := idp.IdentityProviderRepository{} + idpRepo.Init(organisationId) + + exists, err := idpRepo.IsIdentityProviderExist() + if err != nil || exists >= 1 { + m := fmt.Sprintf("External IDP exists; Try to update instead of create; Failed to create identity provider for %v", organisationId) + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + + // validating request payload + valid, err := govalidator.ValidateStruct(idpReq) + if !valid { + m := fmt.Sprintf("Missing mandatory params for creating identity provider for org:%v\n", organisationId) + common.HandleError(w, http.StatusBadRequest, m, err) + return + } + + var newIdentityProvider idp.IdentityProvider + + // OpenID config + newIdentityProvider.Id = primitive.NewObjectID().Hex() + newIdentityProvider.AuthorizationURL = idpReq.Idp.AuthorizationURL + newIdentityProvider.TokenURL = idpReq.Idp.TokenURL + newIdentityProvider.LogoutURL = idpReq.Idp.LogoutURL + newIdentityProvider.ClientID = idpReq.Idp.ClientID + newIdentityProvider.ClientSecret = idpReq.Idp.ClientSecret + newIdentityProvider.JWKSURL = idpReq.Idp.JWKSURL + newIdentityProvider.UserInfoURL = idpReq.Idp.UserInfoURL + newIdentityProvider.DefaultScope = idpReq.Idp.DefaultScope + newIdentityProvider.IssuerUrl = idpReq.Idp.IssuerUrl + newIdentityProvider.OrganisationId = organisationId + newIdentityProvider.IsDeleted = false + + savedIdp, err := idpRepo.Add(newIdentityProvider) + if err != nil { + m := fmt.Sprintf("Failed to create new idp: %v", organisationId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + resp := addIdpResp{ + Idp: savedIdp, + } + response, _ := json.Marshal(resp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/v2/handler/idp/config_delete_idp.go b/src/v2/handler/idp/config_delete_idp.go new file mode 100644 index 0000000..9b14208 --- /dev/null +++ b/src/v2/handler/idp/config_delete_idp.go @@ -0,0 +1,55 @@ +package idp + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/idp" + "github.com/gorilla/mux" +) + +type deleteIdpResp struct { + Idp idp.IdentityProvider `json:"idp" valid:"required"` +} + +// DeleteIdentityProvider Delete external identity provider for an organisation +func DeleteIdentityProvider(w http.ResponseWriter, r *http.Request) { + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + // Path params + idpId := mux.Vars(r)[config.IdpId] + idpId = common.Sanitize(idpId) + + // Repository + idpRepo := idp.IdentityProviderRepository{} + idpRepo.Init(organisationId) + + toBeDeletedIdp, err := idpRepo.Get(idpId) + if err != nil { + m := fmt.Sprintf("Failed to fetch identity provider: %v", idpId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + toBeDeletedIdp.IsDeleted = true + + savedIdp, err := idpRepo.Update(toBeDeletedIdp) + if err != nil { + m := fmt.Sprintf("Failed to delete idp: %v", idpId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + resp := deleteIdpResp{ + Idp: savedIdp, + } + response, _ := json.Marshal(resp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/v2/handler/idp/config_get_idp.go b/src/v2/handler/idp/config_get_idp.go new file mode 100644 index 0000000..81163dc --- /dev/null +++ b/src/v2/handler/idp/config_get_idp.go @@ -0,0 +1,47 @@ +package idp + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/idp" + "github.com/gorilla/mux" +) + +type readIdpResp struct { + Idp idp.IdentityProvider `json:"idp" valid:"required"` +} + +// GetIdentityProvider Get external identity provider for an organisation +func GetIdentityProvider(w http.ResponseWriter, r *http.Request) { + + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + // Path params + idpId := mux.Vars(r)[config.IdpId] + idpId = common.Sanitize(idpId) + + // Repository + idpRepo := idp.IdentityProviderRepository{} + idpRepo.Init(organisationId) + + idp, err := idpRepo.Get(idpId) + if err != nil { + m := fmt.Sprintf("Failed to fetch identity provider: %v", idpId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + resp := readIdpResp{ + Idp: idp, + } + response, _ := json.Marshal(resp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/v2/handler/idp/config_update_idp.go b/src/v2/handler/idp/config_update_idp.go new file mode 100644 index 0000000..3551621 --- /dev/null +++ b/src/v2/handler/idp/config_update_idp.go @@ -0,0 +1,85 @@ +package idp + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/idp" + "github.com/gorilla/mux" +) + +type updateIdpReq struct { + Idp idp.IdentityProvider `json:"idp" valid:"required"` +} + +type updateIdpResp struct { + Idp idp.IdentityProvider `json:"idp" valid:"required"` +} + +// UpdateIdentityProvider Update external identity provider for an organisation +func UpdateIdentityProvider(w http.ResponseWriter, r *http.Request) { + + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + // Path params + idpId := mux.Vars(r)[config.IdpId] + idpId = common.Sanitize(idpId) + + // Request body + var idpReq updateIdpReq + b, _ := io.ReadAll(r.Body) + defer r.Body.Close() + json.Unmarshal(b, &idpReq) + + // validating request payload + valid, err := govalidator.ValidateStruct(idpReq) + if !valid { + m := fmt.Sprintf("Missing mandatory params for creating identity provider for org:%v\n", organisationId) + common.HandleError(w, http.StatusBadRequest, m, err) + return + } + + // Repository + idpRepo := idp.IdentityProviderRepository{} + idpRepo.Init(organisationId) + + toBeUpdatedIndentityProvider, err := idpRepo.Get(idpId) + if err != nil { + m := fmt.Sprintf("Failed to fetch identity provider: %v", idpId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + // OpenID config + toBeUpdatedIndentityProvider.AuthorizationURL = idpReq.Idp.AuthorizationURL + toBeUpdatedIndentityProvider.TokenURL = idpReq.Idp.TokenURL + toBeUpdatedIndentityProvider.LogoutURL = idpReq.Idp.LogoutURL + toBeUpdatedIndentityProvider.ClientID = idpReq.Idp.ClientID + toBeUpdatedIndentityProvider.ClientSecret = idpReq.Idp.ClientSecret + toBeUpdatedIndentityProvider.JWKSURL = idpReq.Idp.JWKSURL + toBeUpdatedIndentityProvider.UserInfoURL = idpReq.Idp.UserInfoURL + toBeUpdatedIndentityProvider.DefaultScope = idpReq.Idp.DefaultScope + toBeUpdatedIndentityProvider.IssuerUrl = idpReq.Idp.IssuerUrl + + savedIdp, err := idpRepo.Update(toBeUpdatedIndentityProvider) + if err != nil { + m := fmt.Sprintf("Failed to update idp: %v", organisationId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + resp := updateIdpResp{ + Idp: savedIdp, + } + response, _ := json.Marshal(resp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/v2/handler/individual/config_create_individual.go b/src/v2/handler/individual/config_create_individual.go index 6faca0e..988e71c 100644 --- a/src/v2/handler/individual/config_create_individual.go +++ b/src/v2/handler/individual/config_create_individual.go @@ -12,20 +12,13 @@ import ( "github.com/asaskevich/govalidator" "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/iam" "github.com/bb-consent/api/src/v2/individual" "go.mongodb.org/mongo-driver/bson/primitive" ) -var iamConfig config.Iam - -// IamInit Initialize the IAM handler -func IamInit(config *config.Configuration) { - iamConfig = config.Iam - -} - func getClient() *gocloak.GoCloak { - client := gocloak.NewClient(iamConfig.URL) + client := gocloak.NewClient(iam.IamConfig.URL) return client } @@ -40,7 +33,7 @@ func getToken(username string, password string, realm string, client *gocloak.Go } func getAdminToken(client *gocloak.GoCloak) (*gocloak.JWT, error) { - t, err := getToken(iamConfig.AdminUser, iamConfig.AdminPassword, "master", client) + t, err := getToken(iam.IamConfig.AdminUser, iam.IamConfig.AdminPassword, "master", client) return t, err } @@ -53,7 +46,7 @@ func registerUser(iamRegReq iamIndividualRegisterReq, adminToken string, client Username: &iamRegReq.Email, } - iamId, err := client.CreateUser(context.Background(), adminToken, iamConfig.Realm, user) + iamId, err := client.CreateUser(context.Background(), adminToken, iam.IamConfig.Realm, user) if err != nil { return "", err } @@ -152,12 +145,13 @@ func ConfigCreateIndividual(w http.ResponseWriter, r *http.Request) { newIndividual = updateIndividualFromRequestBody(individualReq, newIndividual) newIndividual.OrganisationId = organisationId newIndividual.IsDeleted = false + newIndividual.IsOnboardedFromId = false // Repository individualRepo := individual.IndividualRepository{} individualRepo.Init(organisationId) - // Save the data attribute to db + // Save the individual to db savedIndividual, err := individualRepo.Add(newIndividual) if err != nil { m := fmt.Sprintf("Failed to create new individual: %v", newIndividual.Name) diff --git a/src/v2/handler/individual/config_delete_user.go b/src/v2/handler/individual/config_delete_user.go index 39e1cb4..12a01a1 100644 --- a/src/v2/handler/individual/config_delete_user.go +++ b/src/v2/handler/individual/config_delete_user.go @@ -10,13 +10,14 @@ import ( "github.com/Nerzal/gocloak/v13" "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/iam" "github.com/bb-consent/api/src/v2/individual" "github.com/gorilla/mux" ) // unregisterUser Unregisters an existing user func unregisterUser(iamUserID string, adminToken string, client *gocloak.GoCloak) error { - err := client.DeleteUser(context.Background(), adminToken, iamConfig.Realm, iamUserID) + err := client.DeleteUser(context.Background(), adminToken, iam.IamConfig.Realm, iamUserID) return err } diff --git a/src/v2/handler/individual/config_update_individual.go b/src/v2/handler/individual/config_update_individual.go index 3d2df56..7a70230 100644 --- a/src/v2/handler/individual/config_update_individual.go +++ b/src/v2/handler/individual/config_update_individual.go @@ -12,6 +12,7 @@ import ( "github.com/asaskevich/govalidator" "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/iam" "github.com/bb-consent/api/src/v2/individual" "github.com/gorilla/mux" ) @@ -41,7 +42,7 @@ func updateIamIndividual(iamUpdateReq iamIndividualUpdateReq, iamID string) erro log.Printf("Failed to get admin token, user: %v update err:%v", iamUpdateReq.Firstname, err) return err } - user, err := client.GetUserByID(context.Background(), t.AccessToken, iamConfig.Realm, iamID) + user, err := client.GetUserByID(context.Background(), t.AccessToken, iam.IamConfig.Realm, iamID) if err != nil { return err } @@ -50,7 +51,7 @@ func updateIamIndividual(iamUpdateReq iamIndividualUpdateReq, iamID string) erro user.Email = gocloak.StringP(iamUpdateReq.Email) u := *user - err = client.UpdateUser(context.Background(), t.AccessToken, iamConfig.Realm, u) + err = client.UpdateUser(context.Background(), t.AccessToken, iam.IamConfig.Realm, u) return err } diff --git a/src/v2/handler/getorganizationbyid_handler.go b/src/v2/handler/onboard/getorganizationbyid_handler.go similarity index 99% rename from src/v2/handler/getorganizationbyid_handler.go rename to src/v2/handler/onboard/getorganizationbyid_handler.go index 980d036..9ba137f 100644 --- a/src/v2/handler/getorganizationbyid_handler.go +++ b/src/v2/handler/onboard/getorganizationbyid_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" diff --git a/src/v2/handler/getorganizationcoverimage_handler.go b/src/v2/handler/onboard/getorganizationcoverimage_handler.go similarity index 98% rename from src/v2/handler/getorganizationcoverimage_handler.go rename to src/v2/handler/onboard/getorganizationcoverimage_handler.go index 88ed959..4737a33 100644 --- a/src/v2/handler/getorganizationcoverimage_handler.go +++ b/src/v2/handler/onboard/getorganizationcoverimage_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "fmt" diff --git a/src/v2/handler/getorganizationlogoimage_handler.go b/src/v2/handler/onboard/getorganizationlogoimage_handler.go similarity index 98% rename from src/v2/handler/getorganizationlogoimage_handler.go rename to src/v2/handler/onboard/getorganizationlogoimage_handler.go index c3b244e..a2164e8 100644 --- a/src/v2/handler/getorganizationlogoimage_handler.go +++ b/src/v2/handler/onboard/getorganizationlogoimage_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "fmt" diff --git a/src/v2/handler/gettoken_handler.go b/src/v2/handler/onboard/gettoken_handler.go similarity index 90% rename from src/v2/handler/gettoken_handler.go rename to src/v2/handler/onboard/gettoken_handler.go index 4aa9b1e..a772a14 100644 --- a/src/v2/handler/gettoken_handler.go +++ b/src/v2/handler/onboard/gettoken_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" @@ -11,6 +11,7 @@ import ( "github.com/asaskevich/govalidator" "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/iam" ) type tokenReq struct { @@ -39,7 +40,7 @@ func GetToken(w http.ResponseWriter, r *http.Request) { data.Add("client_id", tReq.ClientID) data.Add("grant_type", "refresh_token") - resp, err := http.PostForm(iamConfig.URL+"/realms/"+iamConfig.Realm+"/protocol/openid-connect/token", data) + resp, err := http.PostForm(iam.IamConfig.URL+"/realms/"+iam.IamConfig.Realm+"/protocol/openid-connect/token", data) if err != nil { //m := fmt.Sprintf("Failed to get token from refresh token for user:%v", token.GetUserName(r)) m := fmt.Sprintf("Failed to get token from refresh token") @@ -56,7 +57,7 @@ func GetToken(w http.ResponseWriter, r *http.Request) { } if resp.StatusCode != http.StatusOK { - var e iamError + var e iam.IamError json.Unmarshal(body, &e) response, _ := json.Marshal(e) w.WriteHeader(resp.StatusCode) @@ -65,7 +66,7 @@ func GetToken(w http.ResponseWriter, r *http.Request) { return } - var tok iamToken + var tok iam.IamToken json.Unmarshal(body, &tok) tResp := tokenResp{ AccessToken: tok.AccessToken, diff --git a/src/v2/handler/loginadminuser_handler.go b/src/v2/handler/onboard/loginadminuser_handler.go similarity index 85% rename from src/v2/handler/loginadminuser_handler.go rename to src/v2/handler/onboard/loginadminuser_handler.go index 5ee9a2d..26dbb56 100644 --- a/src/v2/handler/loginadminuser_handler.go +++ b/src/v2/handler/onboard/loginadminuser_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" @@ -13,6 +13,7 @@ import ( "github.com/bb-consent/api/src/config" "github.com/bb-consent/api/src/token" "github.com/bb-consent/api/src/user" + "github.com/bb-consent/api/src/v2/iam" ) type loginReq struct { @@ -45,24 +46,18 @@ func LoginAdminUser(w http.ResponseWriter, r *http.Request) { common.HandleErrorV2(w, http.StatusBadRequest, err.Error(), err) return } + client := iam.GetClient() - t, status, iamErr, err := getToken(lReq.Username, lReq.Password, "igrant-ios-app", iamConfig.Realm) + t, err := iam.GetToken(lReq.Username, lReq.Password, iam.IamConfig.Realm, client) if err != nil { - if (iamError{}) != iamErr { - resp, _ := json.Marshal(iamErr) - w.WriteHeader(status) - w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) - w.Write(resp) - return - } m := fmt.Sprintf("Failed to get token for user:%v", lReq.Username) - common.HandleErrorV2(w, status, m, err) + common.HandleErrorV2(w, http.StatusBadRequest, m, err) return } accessToken, err := token.ParseToken(t.AccessToken) if err != nil { m := fmt.Sprintf("Failed to parse token for user:%v", lReq.Username) - common.HandleErrorV2(w, status, m, err) + common.HandleErrorV2(w, http.StatusBadRequest, m, err) return } diff --git a/src/v2/handler/loginuser_handler.go b/src/v2/handler/onboard/loginuser_handler.go similarity index 81% rename from src/v2/handler/loginuser_handler.go rename to src/v2/handler/onboard/loginuser_handler.go index 15d7ee7..38b394c 100644 --- a/src/v2/handler/loginuser_handler.go +++ b/src/v2/handler/onboard/loginuser_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" @@ -12,6 +12,7 @@ import ( "github.com/bb-consent/api/src/config" "github.com/bb-consent/api/src/token" "github.com/bb-consent/api/src/user" + "github.com/bb-consent/api/src/v2/iam" ) type tokenResp struct { @@ -47,30 +48,25 @@ func LoginUser(w http.ResponseWriter, r *http.Request) { return } - t, status, iamErr, err := getToken(lReq.Username, lReq.Password, "igrant-ios-app", iamConfig.Realm) + client := iam.GetClient() + + t, err := iam.GetToken(lReq.Username, lReq.Password, iam.IamConfig.Realm, client) if err != nil { - if (iamError{}) != iamErr { - resp, _ := json.Marshal(iamErr) - w.WriteHeader(status) - w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) - w.Write(resp) - return - } m := fmt.Sprintf("Failed to get token for user:%v", lReq.Username) - common.HandleErrorV2(w, status, m, err) + common.HandleErrorV2(w, http.StatusBadRequest, m, err) return } accessToken, err := token.ParseToken(t.AccessToken) if err != nil { m := fmt.Sprintf("Failed to parse token for user:%v", lReq.Username) - common.HandleErrorV2(w, status, m, err) + common.HandleErrorV2(w, http.StatusBadRequest, m, err) return } u, err := user.GetByIamIDV2(accessToken.IamID) if err != nil { m := fmt.Sprintf("User: %v does not exist", lReq.Username) - common.HandleErrorV2(w, status, m, err) + common.HandleErrorV2(w, http.StatusBadRequest, m, err) return } tResp := tokenResp{ diff --git a/src/v2/handler/onboard/onboard_exchange_authorization_code.go b/src/v2/handler/onboard/onboard_exchange_authorization_code.go new file mode 100644 index 0000000..f6b9e4e --- /dev/null +++ b/src/v2/handler/onboard/onboard_exchange_authorization_code.go @@ -0,0 +1,167 @@ +package onboard + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/v2/idp" + "github.com/bb-consent/api/src/v2/individual" + "github.com/coreos/go-oidc/v3/oidc" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "golang.org/x/oauth2" +) + +type userInfoResp struct { + Subject string `json:"subject"` + Profile string `json:"profile"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` +} +type tResp struct { + AccessToken string `json:"accessToken"` + ExpiresIn int `json:"expiresIn"` + RefreshExpiresIn int `json:"refreshExpiresIn"` + RefreshToken string `json:"refreshToken"` + TokenType string `json:"tokenType"` +} + +type exchangeAuthorizationResp struct { + UserInfo userInfoResp `json:"userInfo"` + Token tResp `json:"token"` +} + +// ExchangeAuthorizationCode Exchange the authorization code for an access token +func ExchangeAuthorizationCode(w http.ResponseWriter, r *http.Request) { + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + // Fetch query params - redirect_uri, code + oauthRedirectURI := r.URL.Query().Get("redirect_uri") + oauthAuthorizationCode := r.URL.Query().Get("code") + + if oauthRedirectURI == "" || oauthAuthorizationCode == "" { + log.Printf("Missing mandatory query params redirect_uri or code for exchanging authorization code \n") + m := fmt.Sprintf("Failed to exchange authorization code for org:%v", organisationId) + common.HandleError(w, http.StatusNotFound, m, errors.New(m)) + return + } + + // Repository + idpRepo := idp.IdentityProviderRepository{} + idpRepo.Init(organisationId) + + individualRepo := individual.IndividualRepository{} + individualRepo.Init(organisationId) + + // Fetch IDP details based on org Id + idp, err := idpRepo.GetByOrgId() + if err != nil { + m := fmt.Sprintf("failed to fetch idp of individual:%v", organisationId) + common.HandleError(w, http.StatusNotFound, m, err) + return + } + + provider, err := oidc.NewProvider(context.Background(), idp.IssuerUrl) + if err != nil { + m := "failed to initialize oidc provider" + common.HandleError(w, http.StatusNotFound, m, err) + return + } + + // Initialize the OAuth2 configuration + oauth2Config := &oauth2.Config{ + ClientID: idp.ClientID, + ClientSecret: idp.ClientSecret, + RedirectURL: oauthRedirectURI, + Endpoint: provider.Endpoint(), + } + + token, err := oauth2Config.Exchange(context.Background(), oauthAuthorizationCode) + if err != nil { + m := "failed to exchange token" + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + var verifier = provider.Verifier(&oidc.Config{ClientID: idp.ClientID}) + + // Extract the ID Token from OAuth2 token. + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + m := "Failed to extract token" + common.HandleError(w, http.StatusNotFound, m, err) + return + } + + // Parse and verify token payload. + _, err = verifier.Verify(context.Background(), rawIDToken) + if err != nil { + m := "Failed to parse and verify token payload" + common.HandleError(w, http.StatusNotFound, m, err) + return + } + // Fetch user information from the UserInfo endpoint + userInfo, err := provider.UserInfo(context.Background(), oauth2.StaticTokenSource(token)) + if err != nil { + m := "Failed to fetch user information from the UserInfo endpoint" + common.HandleError(w, http.StatusNotFound, m, err) + return + } + individualEmail := userInfo.Email + individualExternalId := userInfo.Subject + + _, err = individualRepo.GetByExternalId(individualExternalId) + if err != nil { + if err == mongo.ErrNoDocuments { + log.Printf("Individual doesn't exist, creating individual.") + createIndividualFromIdp(individualEmail, individualExternalId, organisationId) + } else { + m := fmt.Sprintf("Failed to fetch individual: %v", individualExternalId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + } + t := tResp{ + AccessToken: token.AccessToken, + ExpiresIn: token.Expiry.Minute(), + RefreshToken: token.RefreshToken, + TokenType: token.TokenType, + } + u := userInfoResp{ + Subject: userInfo.Subject, + Profile: userInfo.Profile, + Email: userInfo.Email, + EmailVerified: userInfo.EmailVerified, + } + + response, _ := json.Marshal(exchangeAuthorizationResp{u, t}) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} + +func createIndividualFromIdp(email string, externalId string, organisationId string) error { + individualRepo := individual.IndividualRepository{} + individualRepo.Init(organisationId) + + var newIndividual individual.Individual + newIndividual.Id = primitive.NewObjectID().Hex() + newIndividual.Email = email + newIndividual.ExternalId = externalId + newIndividual.OrganisationId = organisationId + newIndividual.IsDeleted = false + newIndividual.IsOnboardedFromId = true + + _, err := individualRepo.Add(newIndividual) + if err != nil { + return err + } + return nil +} diff --git a/src/v2/handler/updateorganization_handler.go b/src/v2/handler/onboard/updateorganization_handler.go similarity index 99% rename from src/v2/handler/updateorganization_handler.go rename to src/v2/handler/onboard/updateorganization_handler.go index 719a7f9..f30640c 100644 --- a/src/v2/handler/updateorganization_handler.go +++ b/src/v2/handler/onboard/updateorganization_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" diff --git a/src/v2/handler/updateorganizationcoverimage_handler.go b/src/v2/handler/onboard/updateorganizationcoverimage_handler.go similarity index 99% rename from src/v2/handler/updateorganizationcoverimage_handler.go rename to src/v2/handler/onboard/updateorganizationcoverimage_handler.go index 7921115..32f6ade 100644 --- a/src/v2/handler/updateorganizationcoverimage_handler.go +++ b/src/v2/handler/onboard/updateorganizationcoverimage_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "bytes" diff --git a/src/v2/handler/updateorganizationlogoimage_handler.go b/src/v2/handler/onboard/updateorganizationlogoimage_handler.go similarity index 99% rename from src/v2/handler/updateorganizationlogoimage_handler.go rename to src/v2/handler/onboard/updateorganizationlogoimage_handler.go index 4f98a6d..a521914 100644 --- a/src/v2/handler/updateorganizationlogoimage_handler.go +++ b/src/v2/handler/onboard/updateorganizationlogoimage_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "bytes" diff --git a/src/v2/handler/validatephonenumber_handler.go b/src/v2/handler/onboard/validatephonenumber_handler.go similarity index 99% rename from src/v2/handler/validatephonenumber_handler.go rename to src/v2/handler/onboard/validatephonenumber_handler.go index 2cf458e..8de5ec4 100644 --- a/src/v2/handler/validatephonenumber_handler.go +++ b/src/v2/handler/onboard/validatephonenumber_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" diff --git a/src/v2/handler/validateuseremail_handler.go b/src/v2/handler/onboard/validateuseremail_handler.go similarity index 99% rename from src/v2/handler/validateuseremail_handler.go rename to src/v2/handler/onboard/validateuseremail_handler.go index ea214b9..3e84b56 100644 --- a/src/v2/handler/validateuseremail_handler.go +++ b/src/v2/handler/onboard/validateuseremail_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" diff --git a/src/v2/handler/verifyotp_handler.go b/src/v2/handler/onboard/verifyotp_handler.go similarity index 99% rename from src/v2/handler/verifyotp_handler.go rename to src/v2/handler/onboard/verifyotp_handler.go index ea1ba97..15f3182 100644 --- a/src/v2/handler/verifyotp_handler.go +++ b/src/v2/handler/onboard/verifyotp_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "encoding/json" diff --git a/src/v2/handler/verifyphonenumber_handler.go b/src/v2/handler/onboard/verifyphonenumber_handler.go similarity index 95% rename from src/v2/handler/verifyphonenumber_handler.go rename to src/v2/handler/onboard/verifyphonenumber_handler.go index a289b0e..b68b3ce 100644 --- a/src/v2/handler/verifyphonenumber_handler.go +++ b/src/v2/handler/onboard/verifyphonenumber_handler.go @@ -1,4 +1,4 @@ -package handler +package onboard import ( "crypto/rand" @@ -16,6 +16,7 @@ import ( "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" "github.com/bb-consent/api/src/otp" + "github.com/bb-consent/api/src/v2/twilio" ) type verifyPhoneNumberReq struct { @@ -42,7 +43,7 @@ func generateVerificationCode() (code string, err error) { } func sendPhoneVerificationMessage(msgTo string, message string) error { - urlStr := "https://api.twilio.com/2010-04-01/Accounts/" + twilioConfig.AccountSid + "/Messages.json" + urlStr := "https://api.twilio.com/2010-04-01/Accounts/" + twilio.TwilioConfig.AccountSid + "/Messages.json" // Pack up the data for our message msgData := url.Values{} @@ -66,7 +67,7 @@ func sendPhoneVerificationMessage(msgTo string, message string) error { // Create HTTP request client client := &http.Client{} req, _ := http.NewRequest("POST", urlStr, &msgDataReader) - req.SetBasicAuth(twilioConfig.AccountSid, twilioConfig.AuthToken) + req.SetBasicAuth(twilio.TwilioConfig.AccountSid, twilio.TwilioConfig.AuthToken) req.Header.Add("Accept", config.ContentTypeJSON) req.Header.Add(config.ContentTypeHeader, config.ContentTypeFormURLEncoded) diff --git a/src/v2/handler/updateidentityprovider_handler.go b/src/v2/handler/updateidentityprovider_handler.go deleted file mode 100644 index f18f55c..0000000 --- a/src/v2/handler/updateidentityprovider_handler.go +++ /dev/null @@ -1,279 +0,0 @@ -package handler - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "log" - "net/http" - "strings" - - "github.com/asaskevich/govalidator" - "github.com/bb-consent/api/src/common" - "github.com/bb-consent/api/src/config" - "github.com/bb-consent/api/src/org" -) - -// Helper function to update identity provider to iGrant.io IAM -func updateIdentityProvider(identityProviderAlias string, identityProviderRepresentation org.IdentityProviderRepresentation, adminToken string) (int, iamError, error) { - var e iamError - var status = http.StatusInternalServerError - jsonReq, _ := json.Marshal(identityProviderRepresentation) - req, err := http.NewRequest("PUT", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/identity-provider/instances/"+identityProviderAlias, bytes.NewBuffer(jsonReq)) - if err != nil { - return status, e, err - } - - req.Header.Add("Authorization", "Bearer "+adminToken) - req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) - - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Do(req) - if err != nil { - return status, e, err - } - - if resp.StatusCode != http.StatusNoContent { - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - type errorMsg struct { - ErrorMessage string `json:"errorMessage"` - } - var errMsg errorMsg - json.Unmarshal(body, &errMsg) - e.Error = errMsg.ErrorMessage - e.ErrorType = "Identity provider update failed" - return resp.StatusCode, e, errors.New("failed to update identity provider") - } - - defer resp.Body.Close() - - return resp.StatusCode, e, err -} - -func getClientsInRealm(clientID string, adminToken string) (string, int, iamError, error) { - var e iamError - var status = http.StatusInternalServerError - - req, err := http.NewRequest("GET", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/clients?clientId="+clientID, nil) - if err != nil { - return "", status, e, err - } - - req.Header.Add("Authorization", "Bearer "+adminToken) - req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) - - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Do(req) - if err != nil { - return "", status, e, err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - type errorMsg struct { - ErrorMessage string `json:"errorMessage"` - } - var errMsg errorMsg - json.Unmarshal(body, &errMsg) - - e.Error = errMsg.ErrorMessage - e.ErrorType = "OpenID client secret generation failed" - return "", resp.StatusCode, e, errors.New("failed to generate secret for OpenID client") - } - - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - var responseBody []map[string]interface{} - json.Unmarshal(body, &responseBody) - - defer resp.Body.Close() - - return responseBody[0]["id"].(string), resp.StatusCode, e, err - -} - -// Helper function to update OpenID client to manage login sessions for the external identity provider -func updateOpenIDClient(clientUUID string, keycloakOpenIDClient org.KeycloakOpenIDClient, adminToken string) (int, iamError, error) { - - var e iamError - var status = http.StatusInternalServerError - jsonReq, _ := json.Marshal(keycloakOpenIDClient) - req, err := http.NewRequest("PUT", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/clients/"+clientUUID, bytes.NewBuffer(jsonReq)) - if err != nil { - return status, e, err - } - - req.Header.Add("Authorization", "Bearer "+adminToken) - req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) - - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Do(req) - if err != nil { - return status, e, err - } - - if resp.StatusCode != http.StatusNoContent { - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - type errorMsg struct { - ErrorMessage string `json:"errorMessage"` - } - var errMsg errorMsg - json.Unmarshal(body, &errMsg) - e.Error = errMsg.ErrorMessage - e.ErrorType = "OpenID client update failed" - return resp.StatusCode, e, errors.New("failed to update OpenID client") - } - - body, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - var responseBody interface{} - json.Unmarshal(body, &responseBody) - - return resp.StatusCode, e, err - -} - -// UpdateIdentityProvider Update external identity provider for an organisation -func UpdateIdentityProvider(w http.ResponseWriter, r *http.Request) { - - // Get the org ID and fetch the organization from the db. - organizationID := r.Header.Get(config.OrganizationId) - o, err := org.Get(organizationID) - - if err != nil { - m := fmt.Sprintf("Failed to fetch org; Failed to update identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - if !o.ExternalIdentityProviderAvailable { - m := fmt.Sprintf("External IDP provider doesn't exist; Try to create instead of update; Failed to create identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - // Deserializing the request payload to struct - b, _ := ioutil.ReadAll(r.Body) - defer r.Body.Close() - - var updateIdentityProviderReq IdentityProviderReq - json.Unmarshal(b, &updateIdentityProviderReq) - - // validating request payload - valid, err := govalidator.ValidateStruct(updateIdentityProviderReq) - if !valid { - m := fmt.Sprintf("Missing mandatory params for updating identity provider for org:%v\n", organizationID) - common.HandleError(w, http.StatusBadRequest, m, err) - return - } - - var identityProviderOpenIDConfig org.IdentityProviderOpenIDConfig - - // Update OpenID config - identityProviderOpenIDConfig.AuthorizationURL = updateIdentityProviderReq.AuthorizationURL - identityProviderOpenIDConfig.TokenURL = updateIdentityProviderReq.TokenURL - identityProviderOpenIDConfig.LogoutURL = updateIdentityProviderReq.LogoutURL - identityProviderOpenIDConfig.ClientID = updateIdentityProviderReq.ClientID - identityProviderOpenIDConfig.ClientSecret = updateIdentityProviderReq.ClientSecret - identityProviderOpenIDConfig.JWKSURL = updateIdentityProviderReq.JWKSURL - identityProviderOpenIDConfig.UserInfoURL = updateIdentityProviderReq.UserInfoURL - identityProviderOpenIDConfig.ValidateSignature = updateIdentityProviderReq.ValidateSignature - identityProviderOpenIDConfig.DefaultScope = updateIdentityProviderReq.DefaultScope - - if len(strings.TrimSpace(updateIdentityProviderReq.LogoutURL)) > 0 { - identityProviderOpenIDConfig.BackchannelSupported = true - } else { - identityProviderOpenIDConfig.BackchannelSupported = false - } - - identityProviderOpenIDConfig.DisableUserInfo = updateIdentityProviderReq.DisableUserInfo - identityProviderOpenIDConfig.Issuer = updateIdentityProviderReq.Issuer - - if len(strings.TrimSpace(updateIdentityProviderReq.JWKSURL)) > 0 { - identityProviderOpenIDConfig.UseJWKSURL = true - } else { - identityProviderOpenIDConfig.UseJWKSURL = false - } - - identityProviderOpenIDConfig.SyncMode = "IMPORT" - identityProviderOpenIDConfig.ClientAuthMethod = "client_secret_post" - identityProviderOpenIDConfig.HideOnLoginPage = true - - // Fetch admin token from keycloak - t, status, _, err := getAdminToken() - if err != nil { - m := fmt.Sprintf("Failed to fetch the admin token from keycloak; Failed to update identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Updating identity provider OpenID config - o.IdentityProviderRepresentation.Config = identityProviderOpenIDConfig - - // Update identity provider in iGrant.io IAM - status, _, err = updateIdentityProvider(o.IdentityProviderRepresentation.Alias, o.IdentityProviderRepresentation, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to create external identity provider in keycloak; Failed to update identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // Update the identity provider details to the database - o, err = org.UpdateIdentityProviderByOrgID(organizationID, o.IdentityProviderRepresentation) - if err != nil { - m := fmt.Sprintf("Failed to update IDP config to database; Failed to update identity provider for %v", organizationID) - common.HandleError(w, status, m, err) - return - } - - // FIX ME : Is this right practice to do it anonymous function executed in a separate thread ? - go func() { - // Update the OpenID client - o.KeycloakOpenIDClient.Attributes.BackchannelLogoutURL = updateIdentityProviderReq.LogoutURL - - // Fetch OpenID client UUID - openIDClientUUID, _, _, err := getClientsInRealm(o.KeycloakOpenIDClient.ClientID, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to fetch OpenID client UUID from keycloak; Failed to update identity provider for %v", organizationID) - common.HandleError(w, http.StatusNotFound, m, err) - return - } - - // Update OpenID client to iGrant.io IAM - status, _, err = updateOpenIDClient(openIDClientUUID, o.KeycloakOpenIDClient, t.AccessToken) - if err != nil { - m := fmt.Sprintf("Failed to udpate OpenID client in keycloak; Failed to update identity provider for %v", organizationID) - log.Println(m) - return - } - - // Update the OpenID client details to the database - o, err = org.UpdateOpenIDClientByOrgID(organizationID, o.KeycloakOpenIDClient) - if err != nil { - m := fmt.Sprintf("Failed to update OpenID client config to database; Failed to update identity provider for %v", organizationID) - log.Println(m) - return - } - }() - - response, _ := json.Marshal(o.IdentityProviderRepresentation.Config) - w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) - w.WriteHeader(http.StatusOK) - w.Write(response) -} diff --git a/src/v2/http_path/onboard_paths.go b/src/v2/http_path/onboard_paths.go index b594477..0d6c070 100644 --- a/src/v2/http_path/onboard_paths.go +++ b/src/v2/http_path/onboard_paths.go @@ -10,6 +10,7 @@ const VerifyPhoneNumber = "/v2/onboard/verify/phone" const VerifyOtp = "/v2/onboard/verify/otp" const GetToken = "/v2/onboard/token/refresh" +const ExchangeAuthorizationCode = "/v2/onboard/token/exchange" const GetOrganizationByID = "/v2/onboard/organisation" const UpdateOrganization = "/v2/onboard/organisation" diff --git a/src/v2/http_path/routes.go b/src/v2/http_path/routes.go index a59a340..79a6571 100644 --- a/src/v2/http_path/routes.go +++ b/src/v2/http_path/routes.go @@ -1,13 +1,15 @@ package http_path import ( - m "github.com/bb-consent/api/src/middleware" v2Handler "github.com/bb-consent/api/src/v2/handler" dataAgreementHandler "github.com/bb-consent/api/src/v2/handler/dataagreement" dataAttributeHandler "github.com/bb-consent/api/src/v2/handler/dataattribute" + idpHandler "github.com/bb-consent/api/src/v2/handler/idp" individualHandler "github.com/bb-consent/api/src/v2/handler/individual" + onboardHandler "github.com/bb-consent/api/src/v2/handler/onboard" policyHandler "github.com/bb-consent/api/src/v2/handler/policy" webhookHandler "github.com/bb-consent/api/src/v2/handler/webhook" + m "github.com/bb-consent/api/src/v2/middleware" "github.com/casbin/casbin/v2" "github.com/gorilla/mux" ) @@ -55,10 +57,10 @@ func SetRoutes(r *mux.Router, e *casbin.Enforcer) { r.Handle(ConfigListWebhookPayloadContentTypes, m.Chain(webhookHandler.ConfigListWebhookPayloadContentTypes, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") // Organisation identity provider related API(s) - r.Handle(AddIdentityProvider, m.Chain(v2Handler.AddIdentityProvider, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("POST") - r.Handle(UpdateIdentityProvider, m.Chain(v2Handler.UpdateIdentityProvider, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("PUT") - r.Handle(DeleteIdentityProvider, m.Chain(v2Handler.DeleteIdentityProvider, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("DELETE") - r.Handle(GetIdentityProvider, m.Chain(v2Handler.GetIdentityProvider, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("GET") + r.Handle(AddIdentityProvider, m.Chain(idpHandler.ConfigCreateIdp, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("POST") + r.Handle(UpdateIdentityProvider, m.Chain(idpHandler.UpdateIdentityProvider, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("PUT") + r.Handle(DeleteIdentityProvider, m.Chain(idpHandler.DeleteIdentityProvider, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("DELETE") + r.Handle(GetIdentityProvider, m.Chain(idpHandler.GetIdentityProvider, m.Logger(), m.Authorize(e), m.Authenticate(), m.AddContentType())).Methods("GET") // Individual related api(s) r.Handle(ConfigReadIndividual, m.Chain(individualHandler.ConfigReadIndividual, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") @@ -106,20 +108,21 @@ func SetRoutes(r *mux.Router, e *casbin.Enforcer) { // Onboard api(s) - r.Handle(LoginAdminUser, m.Chain(v2Handler.LoginAdminUser, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") - r.Handle(LoginUser, m.Chain(v2Handler.LoginUser, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") + r.Handle(LoginAdminUser, m.Chain(onboardHandler.LoginAdminUser, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") + r.Handle(LoginUser, m.Chain(onboardHandler.LoginUser, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") - r.Handle(ValidateUserEmail, m.Chain(v2Handler.ValidateUserEmail, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") - r.Handle(ValidatePhoneNumber, m.Chain(v2Handler.ValidatePhoneNumber, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") - r.Handle(VerifyPhoneNumber, m.Chain(v2Handler.VerifyPhoneNumber, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") - r.Handle(VerifyOtp, m.Chain(v2Handler.VerifyOtp, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") + r.Handle(ValidateUserEmail, m.Chain(onboardHandler.ValidateUserEmail, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") + r.Handle(ValidatePhoneNumber, m.Chain(onboardHandler.ValidatePhoneNumber, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") + r.Handle(VerifyPhoneNumber, m.Chain(onboardHandler.VerifyPhoneNumber, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") + r.Handle(VerifyOtp, m.Chain(onboardHandler.VerifyOtp, m.LoggerNoAuth(), m.AddContentType())).Methods("POST") - r.Handle(GetToken, m.Chain(v2Handler.GetToken)).Methods("POST") + r.Handle(GetToken, m.Chain(onboardHandler.GetToken)).Methods("POST") + r.Handle(ExchangeAuthorizationCode, m.Chain(onboardHandler.ExchangeAuthorizationCode, m.LoggerNoAuth())).Methods("POST") - r.Handle(GetOrganizationByID, m.Chain(v2Handler.GetOrganizationByID, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") - r.Handle(UpdateOrganization, m.Chain(v2Handler.UpdateOrganization, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("PUT") - r.Handle(UpdateOrganizationCoverImage, m.Chain(v2Handler.UpdateOrganizationCoverImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("POST") - r.Handle(UpdateOrganizationLogoImage, m.Chain(v2Handler.UpdateOrganizationLogoImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("POST") - r.Handle(GetOrganizationCoverImage, m.Chain(v2Handler.GetOrganizationCoverImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") - r.Handle(GetOrganizationLogoImage, m.Chain(v2Handler.GetOrganizationLogoImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") + r.Handle(GetOrganizationByID, m.Chain(onboardHandler.GetOrganizationByID, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") + r.Handle(UpdateOrganization, m.Chain(onboardHandler.UpdateOrganization, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("PUT") + r.Handle(UpdateOrganizationCoverImage, m.Chain(onboardHandler.UpdateOrganizationCoverImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("POST") + r.Handle(UpdateOrganizationLogoImage, m.Chain(onboardHandler.UpdateOrganizationLogoImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("POST") + r.Handle(GetOrganizationCoverImage, m.Chain(onboardHandler.GetOrganizationCoverImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") + r.Handle(GetOrganizationLogoImage, m.Chain(onboardHandler.GetOrganizationLogoImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") } diff --git a/src/v2/iam/iam.go b/src/v2/iam/iam.go new file mode 100644 index 0000000..b56a361 --- /dev/null +++ b/src/v2/iam/iam.go @@ -0,0 +1,46 @@ +package iam + +import ( + "context" + "time" + + "github.com/Nerzal/gocloak/v13" + "github.com/bb-consent/api/src/config" +) + +var IamConfig config.Iam +var Timeout time.Duration + +// IamInit Initialize the IAM +func Init(config *config.Configuration) { + IamConfig = config.Iam + Timeout = time.Duration(time.Duration(IamConfig.Timeout) * time.Second) +} + +func GetToken(username string, password string, realm string, client *gocloak.GoCloak) (*gocloak.JWT, error) { + ctx := context.Background() + token, err := client.LoginAdmin(ctx, username, password, realm) + if err != nil { + return token, err + } + + return token, err +} + +func GetClient() *gocloak.GoCloak { + client := gocloak.NewClient(IamConfig.URL) + return client +} + +type IamToken struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshExpiresIn int `json:"refresh_expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` +} + +type IamError struct { + ErrorType string `json:"error"` + Error string `json:"error_description"` +} diff --git a/src/v2/idp/idp.go b/src/v2/idp/idp.go new file mode 100644 index 0000000..f6f16c8 --- /dev/null +++ b/src/v2/idp/idp.go @@ -0,0 +1,96 @@ +package idp + +import ( + "context" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/database" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +func Collection() *mongo.Collection { + return database.DB.Client.Database(database.DB.Name).Collection("identityProviders") +} + +type IdentityProvider struct { + Id string `json:"id" bson:"_id,omitempty"` + IssuerUrl string `json:"issuerUrl"` + AuthorizationURL string `json:"authorizationUrl" valid:"required"` + TokenURL string `json:"tokenUrl" valid:"required"` + LogoutURL string `json:"logoutUrl" valid:"required"` + ClientID string `json:"clientId" valid:"required"` + ClientSecret string `json:"clientSecret" valid:"required"` + JWKSURL string `json:"jwksUrl" valid:"required"` + UserInfoURL string `json:"userInfoUrl" valid:"required"` + DefaultScope string `json:"defaultScope" valid:"required"` + OrganisationId string `json:"-"` + IsDeleted bool `json:"-"` +} + +type IdentityProviderRepository struct { + DefaultFilter bson.M +} + +// Init +func (idpRepo *IdentityProviderRepository) Init(organisationId string) { + idpRepo.DefaultFilter = bson.M{"organisationid": organisationId, "isdeleted": false} +} + +// IsIdentityProviderExist Check if identity provider exists +func (idpRepo *IdentityProviderRepository) IsIdentityProviderExist() (int64, error) { + + filter := idpRepo.DefaultFilter + + exists, err := Collection().CountDocuments(context.TODO(), filter) + if err != nil { + return exists, err + } + return exists, nil +} + +// Add Adds the identity provider to the db +func (idpRepo *IdentityProviderRepository) Add(idp IdentityProvider) (IdentityProvider, error) { + + _, err := Collection().InsertOne(context.TODO(), idp) + if err != nil { + return IdentityProvider{}, err + } + + return idp, nil +} + +// Update Updates the identity provider +func (idpRepo *IdentityProviderRepository) Update(idp IdentityProvider) (IdentityProvider, error) { + + filter := common.CombineFilters(idpRepo.DefaultFilter, bson.M{"_id": idp.Id}) + update := bson.M{"$set": idp} + + _, err := Collection().UpdateOne(context.TODO(), filter, update) + if err != nil { + return idp, err + } + return idp, nil +} + +// Get Gets a single identity provider by given id +func (idpRepo *IdentityProviderRepository) Get(idpId string) (IdentityProvider, error) { + + filter := common.CombineFilters(idpRepo.DefaultFilter, bson.M{"_id": idpId}) + + var result IdentityProvider + err := Collection().FindOne(context.TODO(), filter).Decode(&result) + + return result, err +} + +// Get Gets a single identity provider by given organisation id +func (idpRepo *IdentityProviderRepository) GetByOrgId() (IdentityProvider, error) { + + filter := idpRepo.DefaultFilter + + var result IdentityProvider + err := Collection().FindOne(context.TODO(), filter).Decode(&result) + + return result, err +} diff --git a/src/v2/individual/individuals.go b/src/v2/individual/individuals.go index 764f9dd..9239e54 100644 --- a/src/v2/individual/individuals.go +++ b/src/v2/individual/individuals.go @@ -22,6 +22,7 @@ type Individual struct { IamId string `json:"iamId"` Email string `json:"email" valid:"required"` Phone string `json:"phone" valid:"required"` + IsOnboardedFromId bool `json:"-"` OrganisationId string `json:"-"` IsDeleted bool `json:"-"` } @@ -69,3 +70,14 @@ func (iRepo *IndividualRepository) Update(individual Individual) (Individual, er } return individual, nil } + +// Get Gets a single individual by given external id +func (iRepo *IndividualRepository) GetByExternalId(externalId string) (Individual, error) { + + filter := common.CombineFilters(iRepo.DefaultFilter, bson.M{"externalid": externalId}) + + var result Individual + err := Collection().FindOne(context.TODO(), filter).Decode(&result) + + return result, err +} diff --git a/src/v2/middleware/application_mode.go b/src/v2/middleware/application_mode.go new file mode 100644 index 0000000..08a9e63 --- /dev/null +++ b/src/v2/middleware/application_mode.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/org" +) + +var ApplicationMode string +var Organization config.Organization + +func ApplicationModeInit(config *config.Configuration) { + ApplicationMode = config.ApplicationMode + Organization = config.Organization +} + +// SetApplicationMode sets application modes for routes to either single tenant or multi tenant +func SetApplicationMode() Middleware { + // Create a new Middleware + return func(f http.HandlerFunc) http.HandlerFunc { + + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + + if ApplicationMode == config.SingleTenant { + + organization, err := org.GetFirstOrganization() + if err != nil { + m := "failed to find organization" + common.HandleError(w, http.StatusBadRequest, m, err) + return + } + organizationId := organization.ID.Hex() + r.Header.Set(config.OrganizationId, organizationId) + } + + // Call the next middleware/handler in chain + f(w, r) + } + } +} diff --git a/src/v2/middleware/authenticate.go b/src/v2/middleware/authenticate.go new file mode 100644 index 0000000..e4ffc99 --- /dev/null +++ b/src/v2/middleware/authenticate.go @@ -0,0 +1,173 @@ +package middleware + +import ( + "context" + "net/http" + + "github.com/bb-consent/api/src/apikey" + "github.com/bb-consent/api/src/org" + "github.com/bb-consent/api/src/token" + "github.com/bb-consent/api/src/user" + v1Handlers "github.com/bb-consent/api/src/v1/handler" + "github.com/bb-consent/api/src/v2/error_handler" + "github.com/bb-consent/api/src/v2/iam" + "github.com/bb-consent/api/src/v2/idp" + "github.com/bb-consent/api/src/v2/individual" + "github.com/coreos/go-oidc/v3/oidc" +) + +func getAccessTokenFromHeader(w http.ResponseWriter, r *http.Request) (headerType int, headerValue string) { + headerType, headerValue, err := token.DecodeAuthHeader(r) + if err != nil { + m := "Invalid authorization header, Authorization failed" + error_handler.Exit(http.StatusUnauthorized, m) + } + + return headerType, headerValue +} + +func storeAccessTokenInRequestContext(headerValue string, w http.ResponseWriter, r *http.Request) { + + t, err := token.ParseToken(headerValue) + if err != nil { + m := "Invalid token, Authorization failed" + error_handler.Exit(http.StatusUnauthorized, m) + } + token.Set(r, t) +} + +func verifyTokenAndIdentifyRole(accessToken string, r *http.Request) error { + // verify consent bb token using jwks url + consentBBIssuerUrl := iam.IamConfig.URL + "/realms/" + iam.IamConfig.Realm + consentBBJwksUrl := iam.IamConfig.URL + "/realms/" + iam.IamConfig.Realm + "/protocol/openid-connect/certs" + + jwks := oidc.NewRemoteKeySet(context.Background(), consentBBJwksUrl) + c := oidc.NewVerifier(consentBBIssuerUrl, jwks, &oidc.Config{SkipClientIDCheck: true}) + tokenPayload, err := c.Verify(context.Background(), accessToken) + if err != nil { + // verify idp token using jwks url + organization, err := org.GetFirstOrganization() + if err != nil { + m := "Failed to fetch organisation" + error_handler.Exit(http.StatusInternalServerError, m) + } + // Repository + idpRepo := idp.IdentityProviderRepository{} + idpRepo.Init(organization.ID.Hex()) + + individualRepo := individual.IndividualRepository{} + individualRepo.Init(organization.ID.Hex()) + + // Fetch IDP details based on org Id + idp, err := idpRepo.GetByOrgId() + if err != nil { + m := "Failed to fetch idp by organisation id" + error_handler.Exit(http.StatusInternalServerError, m) + } + + jwks := oidc.NewRemoteKeySet(context.Background(), idp.JWKSURL) + c := oidc.NewVerifier(idp.IssuerUrl, jwks, &oidc.Config{SkipClientIDCheck: true}) + tokenPayload, err := c.Verify(context.Background(), accessToken) + if err != nil { + m := "Failed to verify token" + error_handler.Exit(http.StatusUnauthorized, m) + } + + externalId := tokenPayload.Subject + + _, err = individualRepo.GetByExternalId(externalId) + if err != nil { + m := "User does not exist, Authorization failed" + error_handler.Exit(http.StatusBadRequest, m) + } + + } else { + iamId := tokenPayload.Subject + + user, err := user.GetByIamID(iamId) + if err != nil { + if err != nil { + m := "Failed to get user by iam id" + error_handler.Exit(http.StatusInternalServerError, m) + } + } + + if len(user.Roles) > 0 { + token.SetUserID(r, user.ID.Hex()) + token.SetUserRoles(r, v1Handlers.GetUserRoles(user.Roles)) + } + } + + return nil +} + +func storeUserIdAndUserRolesInRequestContext(w http.ResponseWriter, r *http.Request) { + + u, err := v1Handlers.GetUserByIamID(token.GetIamID(r)) + if err != nil { + m := "User does not exist, Authorization failed" + error_handler.Exit(http.StatusUnauthorized, m) + } + token.SetUserID(r, u.ID.Hex()) + token.SetUserRoles(r, v1Handlers.GetUserRoles(u.Roles)) +} + +func decodeApiKey(headerValue string, w http.ResponseWriter) apikey.Claims { + claims, err := apikey.Decode(headerValue) + + if err != nil { + m := "Invalid token, Authorization failed" + error_handler.Exit(http.StatusUnauthorized, m) + } + + return claims +} + +func performAPIKeyAuthentication(claims apikey.Claims, w http.ResponseWriter, r *http.Request) { + // TODO: Update this with v2 logic for Api keys + u, err := v1Handlers.GetUser(claims.UserID) + if err != nil { + m := "Invalid API Key, Authorization failed" + error_handler.Exit(http.StatusUnauthorized, m) + } + + t := token.AccessToken{} + t.IamID = u.IamID + t.Name = u.Name + t.Email = u.Email + + token.Set(r, t) + token.SetUserID(r, u.ID.Hex()) + token.SetUserRoles(r, v1Handlers.GetUserRoles(u.Roles)) +} + +// Authenticate Validates the token and sets the token to the context. +func Authenticate() Middleware { + + // Create a new Middleware + return func(f http.HandlerFunc) http.HandlerFunc { + + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + // To catch panic and recover the error + // Once the error is recovered respond by + // writing the error to HTTP response + defer error_handler.HandleExit(w) + headerType, headerValue := getAccessTokenFromHeader(w, r) + + if headerType == token.AuthorizationToken { + + storeAccessTokenInRequestContext(headerValue, w, r) + + verifyTokenAndIdentifyRole(headerValue, r) + // storeUserIdAndUserRolesInRequestContext(w, r) + } + if headerType == token.AuthorizationAPIKey { + claims := decodeApiKey(headerValue, w) + performAPIKeyAuthentication(claims, w, r) + } + // Call the next middleware/handler in chain + f(w, r) + } + } +} diff --git a/src/v2/middleware/authorise.go b/src/v2/middleware/authorise.go new file mode 100644 index 0000000..fd19017 --- /dev/null +++ b/src/v2/middleware/authorise.go @@ -0,0 +1,60 @@ +package middleware + +import ( + "fmt" + "log" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/rbac" + "github.com/bb-consent/api/src/token" + "github.com/bb-consent/api/src/user" + "github.com/casbin/casbin/v2" +) + +func Authorize(e *casbin.Enforcer) Middleware { + + // Create a new Middleware + return func(f http.HandlerFunc) http.HandlerFunc { + + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + + userID := token.GetUserID(r) + + user, err := user.Get(userID) + if err != nil { + m := fmt.Sprintf("Failed to locate user with ID: %v", userID) + common.HandleError(w, http.StatusBadRequest, m, err) + return + } + roles := user.Roles + + var role string + + if len(roles) > 0 { + role = rbac.ROLE_ADMIN + } else { + role = rbac.ROLE_USER + } + + // casbin enforce + res, err := e.Enforce(role, r.URL.Path, r.Method) + if err != nil { + m := "Failed to enforce casbin authentication;" + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + if !res { + log.Printf("User does not have enough permissions") + m := "Unauthorized access;User doesn't have enough permissions;" + common.HandleError(w, http.StatusForbidden, m, nil) + return + } + + // Call the next middleware/handler in chain + f(w, r) + } + } +} diff --git a/src/v2/middleware/content_type.go b/src/v2/middleware/content_type.go new file mode 100644 index 0000000..c8e5741 --- /dev/null +++ b/src/v2/middleware/content_type.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "net/http" + + "github.com/bb-consent/api/src/config" +) + +func AddContentType() Middleware { + // Create a new Middleware + return func(f http.HandlerFunc) http.HandlerFunc { + + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Add(config.ContentTypeHeader, config.ContentTypeJSON) + + // Call the next middleware/handler in chain + f(w, r) + } + } +} diff --git a/src/v2/middleware/logger.go b/src/v2/middleware/logger.go new file mode 100644 index 0000000..db69df6 --- /dev/null +++ b/src/v2/middleware/logger.go @@ -0,0 +1,30 @@ +package middleware + +import ( + "log" + "net/http" + "time" + + "github.com/bb-consent/api/src/token" +) + +// Logger logs all requests with its path and the time it took to process +func Logger() Middleware { + + // Create a new Middleware + return func(f http.HandlerFunc) http.HandlerFunc { + + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + + // Do middleware things + start := time.Now() + defer func() { + log.Println("name:", token.GetUserName(r), "id:", token.GetUserID(r), time.Since(start), r.Method, r.URL.Path) + }() + + // Call the next middleware/handler in chain + f(w, r) + } + } +} diff --git a/src/v2/middleware/logger_no_auth.go b/src/v2/middleware/logger_no_auth.go new file mode 100644 index 0000000..3de3544 --- /dev/null +++ b/src/v2/middleware/logger_no_auth.go @@ -0,0 +1,25 @@ +package middleware + +import ( + "log" + "net/http" + "time" +) + +// LoggerNoAuth Logs API(s) that doesnt have tokens in the calls. +func LoggerNoAuth() Middleware { + // Create a new Middleware + return func(f http.HandlerFunc) http.HandlerFunc { + + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + + // Do middleware things + start := time.Now() + defer func() { log.Println(r.URL.Path, time.Since(start)) }() + + // Call the next middleware/handler in chain + f(w, r) + } + } +} diff --git a/src/v2/middleware/middleware.go b/src/v2/middleware/middleware.go new file mode 100644 index 0000000..9ac72cb --- /dev/null +++ b/src/v2/middleware/middleware.go @@ -0,0 +1,16 @@ +package middleware + +import ( + "net/http" +) + +// Middleware Middleware function type declaration +type Middleware func(http.HandlerFunc) http.HandlerFunc + +// Chain applies middlewares to a http.HandlerFunc +func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc { + for _, m := range middlewares { + f = m(f) + } + return f +} diff --git a/src/v2/twilio/twilio.go b/src/v2/twilio/twilio.go new file mode 100644 index 0000000..8e4efba --- /dev/null +++ b/src/v2/twilio/twilio.go @@ -0,0 +1,13 @@ +package twilio + +import ( + "github.com/bb-consent/api/src/config" +) + +var TwilioConfig config.Twilio + +// Init Initialize twilio +func Init(config *config.Configuration) { + TwilioConfig = config.Twilio + +}