From dfb65f78e65ec5b2aa5d41cbc01f12497da88af3 Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Wed, 19 Feb 2020 10:36:59 -0500 Subject: [PATCH 1/9] Adds new config options --- .gitignore | 1 + go.mod | 2 ++ okta/config.go | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 61ead8666..144b8dd7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /vendor +.okta diff --git a/go.mod b/go.mod index 64beb0713..39e6dfd15 100644 --- a/go.mod +++ b/go.mod @@ -12,3 +12,5 @@ require ( gopkg.in/yaml.v2 v2.2.2 // indirect ) + +go 1.13 diff --git a/okta/config.go b/okta/config.go index 3efdc0d28..12c21ae73 100644 --- a/okta/config.go +++ b/okta/config.go @@ -41,8 +41,12 @@ type config struct { RateLimit struct { MaxRetries int32 `yaml:"maxRetries" envconfig:"OKTA_CLIENT_RATE_LIMIT_MAX_RETRIES"` } `yaml:"rateLimit"` - OrgUrl string `yaml:"orgUrl" envconfig:"OKTA_CLIENT_ORGURL"` - Token string `yaml:"token" envconfig:"OKTA_CLIENT_TOKEN"` + OrgUrl string `yaml:"orgUrl" envconfig:"OKTA_CLIENT_ORGURL"` + Token string `yaml:"token" envconfig:"OKTA_CLIENT_TOKEN"` + AuthorizationMode string `yaml:"authorizationMode" envconfig:"OKTA_CLIENT_AUTHORIZATIONMODE"` + ClientId string `yaml:"clientId" envconfig:"OKTA_CLIENT_CLIENTID"` + Scopes []string `yaml:"scopes" envconfig:"OKTA_CLIENT_SCOPES"` + PrivateKey string `yaml:"privateKey" envconfig:"OKTA_CLIENT_PRIVATEKEY"` } `yaml:"client"` Testing struct { DisableHttpsCheck bool `yaml:"disableHttpsCheck" envconfig:"OKTA_TESTING_DISABLE_HTTPS_CHECK"` @@ -150,3 +154,27 @@ func WithRateLimitMaxRetries(maxRetries int32) ConfigSetter { c.Okta.Client.RateLimit.MaxRetries = maxRetries } } + +func WithAuthorizationMode(authzMode string) ConfigSetter { + return func(c *config) { + c.Okta.Client.AuthorizationMode = authzMode + } +} + +func WithClientId(clientId string) ConfigSetter { + return func(c *config) { + c.Okta.Client.ClientId = clientId + } +} + +func WithScopes(scopes []string) ConfigSetter { + return func(c *config) { + c.Okta.Client.Scopes = scopes + } +} + +func WithPrivateKey(privateKey string) ConfigSetter { + return func(c *config) { + c.Okta.Client.PrivateKey = privateKey + } +} From 9a7ecded05fb6c064f7922c66b9bd44f81beae64 Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Mon, 2 Mar 2020 10:55:37 -0500 Subject: [PATCH 2/9] Adds ability to use Private Key access_token for Okta --- go.mod | 10 +++- go.sum | 22 +++++++ okta/cache/cache.go | 2 + okta/cache/goCache.go | 13 +++++ okta/cache/noopCache.go | 8 +++ okta/okta.go | 3 +- okta/requestExecutor.go | 99 +++++++++++++++++++++++++++++++- okta/validator.go | 21 +++++++ tests/unit/client_config_test.go | 18 ++++++ tests/unit/request_test.go | 67 +++++++++++++++++++++ 10 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 tests/unit/request_test.go diff --git a/go.mod b/go.mod index 39e6dfd15..7a9602428 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,19 @@ module github.com/okta/okta-sdk-golang require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-yaml/yaml v2.1.0+incompatible + github.com/google/uuid v1.1.1 github.com/jarcoal/httpmock v1.0.4 github.com/kelseyhightower/envconfig v1.3.0 github.com/kr/pretty v0.1.0 // indirect + github.com/lestrrat-go/jwx v0.9.0 github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 - github.com/stretchr/testify v1.3.0 + github.com/pkg/errors v0.9.1 // indirect + github.com/square/go-jose v2.4.1+incompatible + github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc + github.com/stretchr/testify v1.5.1 + golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect + gopkg.in/square/go-jose.v2 v2.4.1 ) diff --git a/go.sum b/go.sum index bb3f56e72..45e5831e7 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= @@ -13,15 +15,35 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/jwx v0.9.0 h1:Fnd0EWzTm0kFrBPzE/PEPp9nzllES5buMkksPMjEKpM= +github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk= github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y= github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/square/go-jose v2.4.1+incompatible h1:KFYc54wTtgnd3x4B/Y7Zr1s/QaEx2BNzRsB3Hae5LHo= +github.com/square/go-jose v2.4.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8= +github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc h1:E/YAnZeUG5DNF2fOyRjBCO/SPa3bRIQhFWfMFaBcguw= +github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc/go.mod h1:JbpHhNyeVc538vtj/ECJ3gPYm1VEitNjsLhm4eJQQbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= +gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/okta/cache/cache.go b/okta/cache/cache.go index 89a4dfafe..62cc04dea 100644 --- a/okta/cache/cache.go +++ b/okta/cache/cache.go @@ -25,6 +25,8 @@ import ( type Cache interface { Get(key string) *http.Response Set(key string, value *http.Response) + GetString(key string) string + SetString(key string, value string) Delete(key string) Clear() Has(key string) bool diff --git a/okta/cache/goCache.go b/okta/cache/goCache.go index 73723f287..dd8d2dc3e 100644 --- a/okta/cache/goCache.go +++ b/okta/cache/goCache.go @@ -55,6 +55,19 @@ func (c GoCache) Set(key string, value *http.Response) { c.rootLibrary.Set(key, value, c.ttl) } +func (c GoCache) GetString(key string) string { + item, found := c.rootLibrary.Get(key) + if found { + return item.(string) + } + + return "" +} + +func (c GoCache) SetString(key string, value string) { + c.rootLibrary.Set(key, value, c.ttl) +} + func (c GoCache) Delete(key string) { c.rootLibrary.Delete(key) } diff --git a/okta/cache/noopCache.go b/okta/cache/noopCache.go index 333e4677e..c6e223020 100644 --- a/okta/cache/noopCache.go +++ b/okta/cache/noopCache.go @@ -33,6 +33,14 @@ func (c NoOpCache) Set(key string, value *http.Response) { } +func (c NoOpCache) GetString(key string) string { + return "" +} + +func (c NoOpCache) SetString(key string, value string) { + +} + func (c NoOpCache) Delete(key string) { } diff --git a/okta/okta.go b/okta/okta.go index 1c0b669f5..40451f1fe 100644 --- a/okta/okta.go +++ b/okta/okta.go @@ -118,7 +118,8 @@ func setConfigDefaults(c *config) { WithUserAgentExtra(""), WithTestingDisableHttpsCheck(false), WithRequestTimeout(0), - WithRateLimitMaxRetries(2)) + WithRateLimitMaxRetries(2), + WithAuthorizationMode("SSWS")) for _, confSetter := range conf { confSetter(c) diff --git a/okta/requestExecutor.go b/okta/requestExecutor.go index 090736606..b509ce572 100644 --- a/okta/requestExecutor.go +++ b/okta/requestExecutor.go @@ -18,7 +18,9 @@ package okta import ( "bytes" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "io" @@ -28,9 +30,12 @@ import ( "reflect" "sort" "strconv" + "strings" "time" "github.com/okta/okta-sdk-golang/okta/cache" + "github.com/square/go-jose/jwt" + "gopkg.in/square/go-jose.v2" ) type RequestExecutor struct { @@ -40,6 +45,22 @@ type RequestExecutor struct { cache cache.Cache } +type ClientAssertionClaims struct { + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Audience string `json:"aud,omitempty"` + Expiry *jwt.NumericDate `json:"exp,omitempty"` + IssuedAt *jwt.NumericDate `json:"iat,omitempty"` + ID string `json:"jti,omitempty"` +} + +type RequestAccessToken struct { + TokenType string `json:"token_type,omitempty"` + ExpireIn int `json:"expire_in,omitempty"` + AccessToken string `json:"access_token,omitempty"` + Scope string `json:"scope,omitempty"` +} + func NewRequestExecutor(httpClient *http.Client, cache cache.Cache, config *config) *RequestExecutor { re := RequestExecutor{} re.httpClient = httpClient @@ -74,7 +95,83 @@ func (re *RequestExecutor) NewRequest(method string, url string, body interface{ if err != nil { return nil, err } - req.Header.Add("Authorization", "SSWS "+re.config.Okta.Client.Token) + + if re.config.Okta.Client.AuthorizationMode == "SSWS" { + req.Header.Add("Authorization", "SSWS "+re.config.Okta.Client.Token) + } + + if re.config.Okta.Client.AuthorizationMode == "PrivateKey" { + if re.cache.Has("OKTA_ACCESS_TOKEN") { + token := re.cache.GetString("OKTA_ACCESS_TOKEN") + req.Header.Add("Authorization", "Bearer "+token) + } else { + priv := []byte(re.config.Okta.Client.PrivateKey) + + privPem, _ := pem.Decode(priv) + if privPem.Type != "RSA PRIVATE KEY" { + return nil, fmt.Errorf("RSA private key is of the wrong type") + } + + parsedKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) + if err != nil { + return nil, err + } + + signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: parsedKey}, nil) + if err != nil { + return nil, err + } + + claims := ClientAssertionClaims{ + Subject: re.config.Okta.Client.ClientId, + IssuedAt: jwt.NewNumericDate(time.Now()), + Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(1))), + Issuer: re.config.Okta.Client.ClientId, + Audience: re.config.Okta.Client.OrgUrl + "/oauth2/v1/token", + } + jwtBuilder := jwt.Signed(signer).Claims(claims) + clientAssertion, err := jwtBuilder.CompactSerialize() + if err != nil { + return nil, err + } + + var tokenRequestBuff io.ReadWriter + tokenRequestUrl := re.config.Okta.Client.OrgUrl + "/oauth2/v1/token" + tokenRequestUrl += "?grant_type=client_credentials" + tokenRequestUrl += "&scope=" + strings.Join(re.config.Okta.Client.Scopes, " ") + tokenRequestUrl += "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + tokenRequestUrl += "&client_assertion=" + clientAssertion + + tokenRequest, err := http.NewRequest("POST", tokenRequestUrl, tokenRequestBuff) + if err != nil { + return nil, err + } + + tokenRequest.Header.Add("Accept", "application/json") + tokenRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + tokenResponse, err := re.httpClient.Do(tokenRequest) + if err != nil { + return nil, err + } + + respBody, err := ioutil.ReadAll(tokenResponse.Body) + if err != nil { + return nil, err + } + origResp := ioutil.NopCloser(bytes.NewBuffer(respBody)) + tokenResponse.Body = origResp + var accessToken *RequestAccessToken + _, err = buildResponse(tokenResponse, &accessToken) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+accessToken.AccessToken) + + re.cache.SetString("OKTA_ACCESS_TOKEN", accessToken.AccessToken) + } + + } req.Header.Add("User-Agent", NewUserAgent(re.config).String()) req.Header.Add("Accept", "application/json") diff --git a/okta/validator.go b/okta/validator.go index 2a639d7a2..649d65e25 100644 --- a/okta/validator.go +++ b/okta/validator.go @@ -18,6 +18,11 @@ func validateConfig(c *config) (*config, error) { return nil, err } + err = validateAuthorization(c) + if err != nil { + return nil, err + } + return c, nil } @@ -58,3 +63,19 @@ func validateApiToken(c *config) error { } return nil } + +func validateAuthorization(c *config) error { + if c.Okta.Client.AuthorizationMode != "SSWS" && + c.Okta.Client.AuthorizationMode != "PrivateKey" { + return errors.New("the AuthorizaitonMode config option must be one of [SSWS, PrivateKey]. You provided the SDK with " + c.Okta.Client.AuthorizationMode) + } + + if c.Okta.Client.AuthorizationMode == "PrivateKey" && + (c.Okta.Client.ClientId == "" || + c.Okta.Client.Scopes == nil || + c.Okta.Client.PrivateKey == "") { + return errors.New("when using AuthorizationMode 'PrivateKey', you must supply 'ClientId', 'Scopes', and 'PrivateKey'") + } + + return nil +} diff --git a/tests/unit/client_config_test.go b/tests/unit/client_config_test.go index 673f0af4c..1c6638f8d 100644 --- a/tests/unit/client_config_test.go +++ b/tests/unit/client_config_test.go @@ -78,3 +78,21 @@ func Test_panic_when_api_token_contains_placeholder(t *testing.T) { _, _ = tests.NewClient(okta.WithToken("{apiToken}")) }, "Does not panic when api token contains {apiToken}") } + +func Test_panic_when_authorization_mode_is_not_valid(t *testing.T) { + assert.Panics(t, func() { + _, _ = tests.NewClient(okta.WithAuthorizationMode("invalid")) + }, "Does not panic when authorization mode is invalid") +} + +func Test_does_not_panic_when_authorization_mode_is_valid(t *testing.T) { + assert.NotPanics(t, func() { + _, _ = tests.NewClient(okta.WithAuthorizationMode("SSWS")) + }, "Should not panic when authorization mode is SSWS") +} + +func Test_will_panic_if_private_key_authorization_type_with_missing_properties(t *testing.T) { + assert.Panics(t, func() { + _, _ = tests.NewClient(okta.WithAuthorizationMode("PrivateKey")) + }, "Does not panic if private key selected with no other required options") +} diff --git a/tests/unit/request_test.go b/tests/unit/request_test.go new file mode 100644 index 000000000..84ef5da6b --- /dev/null +++ b/tests/unit/request_test.go @@ -0,0 +1,67 @@ +/* + * Copyright 2018 - Present Okta, Inc. + * + * 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 unit + +import ( + "io" + "testing" + + "github.com/okta/okta-sdk-golang/okta" + "github.com/okta/okta-sdk-golang/okta/query" + "github.com/okta/okta-sdk-golang/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_private_key_request_contains_bearer_token(t *testing.T) { + var buff io.ReadWriter + + client, _ := tests.NewClient(okta.WithAuthorizationMode("PrivateKey"), okta.WithScopes(([]string{"okta.users.manage"}))) + + request, _ := client.GetRequestExecutor().NewRequest("GET", "https://example.com/", buff) + + assert.Contains(t, request.Header.Get("Authorization"), "Bearer", "does not contain a bearer token for the request") + +} + +func Test_private_key_request_can_create_a_user(t *testing.T) { + client, _ := tests.NewClient(okta.WithAuthorizationMode("PrivateKey"), okta.WithScopes(([]string{"okta.users.manage"}))) + + p := &okta.PasswordCredential{ + Value: "Abcd1234", + } + uc := &okta.UserCredentials{ + Password: p, + } + profile := okta.UserProfile{} + profile["firstName"] = "John" + profile["lastName"] = "Private_Key" + profile["email"] = "john-private-key@example.com" + profile["login"] = "john-private-key@example.com" + u := &okta.User{ + Credentials: uc, + Profile: &profile, + } + + qp := query.NewQueryParams(query.WithActivate(false)) + + user, _, err := client.User.CreateUser(*u, qp) + require.NoError(t, err, "Creating an user should not error") + assert.NotEmpty(t, user.Id, "appears the user was not created") + tempProfile := *user.Profile + assert.Equal(t, "john-private-key@example.com", tempProfile["email"], "did not get the correct user") +} From b5d1880b8175f854c7befc72ddea24044b879f33 Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Mon, 2 Mar 2020 11:04:44 -0500 Subject: [PATCH 3/9] Update Readme --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 33a5437cd..199eac29b 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,23 @@ client := okta.NewClient(context, okta.WithOrgUrl("https://{yourOktaDomain}"), o Hard-coding the Okta domain and API token works for quick tests, but for real projects you should use a more secure way of storing these values (such as environment variables). This library supports a few different configuration sources, covered in the [configuration reference](#configuration-reference) section. +### OAuth 2.0 + +Okta allows you to interact with Okta APIs using scoped OAuth 2.0 access tokens. Each access token enables the bearer to perform specific actions on specific Okta endpoints, with that ability controlled by which scopes the access token contains. + +This SDK supports this feature only for service-to-service applications. Check out [our guides](https://developer.okta.com/docs/guides/implement-oauth-for-okta/overview/) to learn more about how to register a new service application using a private and public key pair. + +When using this approach you won't need an API Token because the SDK will request an access token for you. In order to use OAuth 2.0, construct a client instance by passing the following parameters: + +``` +client, _ := okta.NewClient(context, + okta.WithAuthorizationMode("PrivateKey"), + okta.WithClientId("{{clientId}}), + okta.WithScopes(([]string{"okta.users.manage"})), + okta.WithPrivateKey({{PEM PRIVATE KEY BLOCK}}) +) +``` + ### Extending the Client When calling `okta.NewClient()` we allow for you to pass custom instances of `http.Client` and `cache.Cache`. @@ -362,7 +379,7 @@ Higher numbers win. In other words, configuration passed via the constructor wil ### YAML configuration -The full YAML configuration looks like: +When you use an API Token instead of OAuth 2.0 the full YAML configuration looks like: ```yaml okta: @@ -377,6 +394,32 @@ okta: token: {apiToken} ``` +When you use OAuth 2.0 the full YAML configuration looks like: + +```yaml +okta: + client: + connectionTimeout: 30 # seconds + oktaDomain: "https://{yourOktaDomain}" + proxy: + port: null + host: null + username: null + password: null + authorizationMode: "PrivateKey" + clientId: "{yourClientId}" + Scopes: scope.1 scope.2 + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAl4F5CrP6Wu2kKwH1Z+CNBdo0iteHhVRIXeHdeoqIB1iXvuv4 + THQdM5PIlot6XmeV1KUKuzw2ewDeb5zcasA4QHPcSVh2+KzbttPQ+RUXCUAr5t+r + 0r6gBc5Dy1IPjCFsqsPJXFwqe3RzUb... + -----END RSA PRIVATE KEY----- + requestTimeout: 0 # seconds + rateLimit: + maxRetries: 4 +``` + ### Environment variables Each one of the configuration values above can be turned into an environment variable name with the `_` (underscore) character: From 2cb0f3280734288c9c5a9865d870b35b56060f56 Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Mon, 2 Mar 2020 12:22:53 -0500 Subject: [PATCH 4/9] Remove user created for private_key test --- tests/unit/request_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/request_test.go b/tests/unit/request_test.go index 84ef5da6b..0e2faad73 100644 --- a/tests/unit/request_test.go +++ b/tests/unit/request_test.go @@ -64,4 +64,12 @@ func Test_private_key_request_can_create_a_user(t *testing.T) { assert.NotEmpty(t, user.Id, "appears the user was not created") tempProfile := *user.Profile assert.Equal(t, "john-private-key@example.com", tempProfile["email"], "did not get the correct user") + + // Deactivate the user → POST /api/v1/users/{{userId}}/lifecycle/deactivate + _, err = client.User.DeactivateUser(user.Id, nil) + require.NoError(t, err, "Should not error when deactivating") + + // Delete the user → DELETE /api/v1/users/{{userId}} + _, err = client.User.DeactivateOrDeleteUser(user.Id, nil) + require.NoError(t, err, "Should not error when deleting") } From 22dc7a8ca033e0900bcfc73066cc01623df9444e Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Wed, 4 Mar 2020 16:54:25 -0500 Subject: [PATCH 5/9] Update Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 199eac29b..db7aaec81 100644 --- a/README.md +++ b/README.md @@ -408,7 +408,7 @@ okta: password: null authorizationMode: "PrivateKey" clientId: "{yourClientId}" - Scopes: scope.1 scope.2 + scopes: scope.1 scope.2 privateKey: | -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAl4F5CrP6Wu2kKwH1Z+CNBdo0iteHhVRIXeHdeoqIB1iXvuv4 From dafd69d58ae2779ca598e39b671e2f7bf9fe4d02 Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Wed, 4 Mar 2020 17:19:33 -0500 Subject: [PATCH 6/9] Update URL Generation for Token Request --- okta/requestExecutor.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/okta/requestExecutor.go b/okta/requestExecutor.go index b509ce572..411552d2a 100644 --- a/okta/requestExecutor.go +++ b/okta/requestExecutor.go @@ -27,6 +27,7 @@ import ( "io/ioutil" "net/http" "net/url" + nUrl "net/url" "reflect" "sort" "strconv" @@ -136,12 +137,14 @@ func (re *RequestExecutor) NewRequest(method string, url string, body interface{ } var tokenRequestBuff io.ReadWriter + query := nUrl.Values{} tokenRequestUrl := re.config.Okta.Client.OrgUrl + "/oauth2/v1/token" - tokenRequestUrl += "?grant_type=client_credentials" - tokenRequestUrl += "&scope=" + strings.Join(re.config.Okta.Client.Scopes, " ") - tokenRequestUrl += "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - tokenRequestUrl += "&client_assertion=" + clientAssertion + query.Add("grant_type", "client_credentials") + query.Add("scope", strings.Join(re.config.Okta.Client.Scopes, " ")) + query.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + query.Add("client_assertion", clientAssertion) + tokenRequestUrl += "?" + query.Encode() tokenRequest, err := http.NewRequest("POST", tokenRequestUrl, tokenRequestBuff) if err != nil { return nil, err From 0b6ad9a6eea1a1b3840fa5b7f35065f130286d4b Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Wed, 4 Mar 2020 17:22:47 -0500 Subject: [PATCH 7/9] Update readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db7aaec81..b9e64b797 100644 --- a/README.md +++ b/README.md @@ -400,7 +400,7 @@ When you use OAuth 2.0 the full YAML configuration looks like: okta: client: connectionTimeout: 30 # seconds - oktaDomain: "https://{yourOktaDomain}" + orgUrl: "https://{yourOktaDomain}" proxy: port: null host: null @@ -408,7 +408,9 @@ okta: password: null authorizationMode: "PrivateKey" clientId: "{yourClientId}" - scopes: scope.1 scope.2 + scopes: + - scope.1 + - scope.2 privateKey: | -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAl4F5CrP6Wu2kKwH1Z+CNBdo0iteHhVRIXeHdeoqIB1iXvuv4 From 125ebc0516d86028d6ee3ce6dfdd5b32534d750a Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Thu, 5 Mar 2020 09:35:40 -0500 Subject: [PATCH 8/9] updates --- README.md | 2 +- okta/validator.go | 8 +++++--- tests/{unit => integration}/request_test.go | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) rename tests/{unit => integration}/request_test.go (99%) diff --git a/README.md b/README.md index b9e64b797..d0bd40f99 100644 --- a/README.md +++ b/README.md @@ -371,7 +371,7 @@ return authServer, resp, nil This library looks for configuration in the following sources: 0. An `okta.yaml` file in a `.okta` folder in the current user's home directory (`~/.okta/okta.yaml` or `%userprofile\.okta\okta.yaml`) -0. An `okta.yaml` file in a `.okta` folder in the application or project's root directory +0. A `.okta.yaml` file in the application or project's root directory 0. Environment variables 0. Configuration explicitly passed to the constructor (see the example in [Getting started](#getting-started)) diff --git a/okta/validator.go b/okta/validator.go index 649d65e25..3980b5f32 100644 --- a/okta/validator.go +++ b/okta/validator.go @@ -13,9 +13,11 @@ func validateConfig(c *config) (*config, error) { return nil, err } - err = validateApiToken(c) - if err != nil { - return nil, err + if c.Okta.Client.AuthorizationMode == "SSWS" { + err = validateApiToken(c) + if err != nil { + return nil, err + } } err = validateAuthorization(c) diff --git a/tests/unit/request_test.go b/tests/integration/request_test.go similarity index 99% rename from tests/unit/request_test.go rename to tests/integration/request_test.go index 0e2faad73..170240c53 100644 --- a/tests/unit/request_test.go +++ b/tests/integration/request_test.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package unit +package integration import ( "io" From 8a113e8ed4d4dc4304f473bb0a6f80ade7b95036 Mon Sep 17 00:00:00 2001 From: Brian Retterer Date: Thu, 5 Mar 2020 14:22:37 -0500 Subject: [PATCH 9/9] Fixes tests --- okta/okta.go | 12 ++++++------ tests/integration/application_test.go | 11 +++++++++++ tests/integration/group_test.go | 2 +- tests/unit/client_config_test.go | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/okta/okta.go b/okta/okta.go index 40451f1fe..e54317c78 100644 --- a/okta/okta.go +++ b/okta/okta.go @@ -126,20 +126,20 @@ func setConfigDefaults(c *config) { } } -func readConfigFromFile(location string) (*config, error) { +func readConfigFromFile(location string, c *config) (*config, error) { yamlConfig, err := ioutil.ReadFile(location) if err != nil { return nil, err } - conf := config{} - err = yaml.Unmarshal(yamlConfig, &conf) + // conf := config{} + err = yaml.Unmarshal(yamlConfig, c) if err != nil { return nil, err } - return &conf, err + return c, err } func readConfigFromSystem(c config) *config { @@ -151,7 +151,7 @@ func readConfigFromSystem(c config) *config { return &c } - conf, err := readConfigFromFile(currUser.HomeDir + "/.okta/okta.yaml") + conf, err := readConfigFromFile(currUser.HomeDir+"/.okta/okta.yaml", &c) if err != nil { return &c @@ -161,7 +161,7 @@ func readConfigFromSystem(c config) *config { } func readConfigFromApplication(c config) *config { - conf, err := readConfigFromFile(".okta.yaml") + conf, err := readConfigFromFile(".okta.yaml", &c) if err != nil { return &c diff --git a/tests/integration/application_test.go b/tests/integration/application_test.go index e486e0883..c6a10ec2a 100644 --- a/tests/integration/application_test.go +++ b/tests/integration/application_test.go @@ -358,6 +358,12 @@ func Test_can_set_application_settings_during_creation(t *testing.T) { assert.IsType(t, okta.BasicApplicationSettingsApplication{}, *application.(*okta.BasicAuthApplication).Settings.App, "The returned type of application settings application was not correct type") assert.Equal(t, "https://example.com/auth.html", application.(*okta.BasicAuthApplication).Settings.App.Url) + + appId := application.(*okta.BasicAuthApplication).Id + client.Application.DeactivateApplication(appId) + _, err = client.Application.DeleteApplication(appId) + + require.NoError(t, err, "Deleting an application should not error") } func Test_can_set_application_settings_during_update(t *testing.T) { @@ -390,4 +396,9 @@ func Test_can_set_application_settings_during_update(t *testing.T) { updatedApp, _, err := client.Application.GetApplication(appId, okta.NewBasicAuthApplication(), nil) assert.Equal(t, "https://okta.com/auth", updatedApp.(*okta.BasicAuthApplication).Settings.App.Url, "The URL was not updated'") + + client.Application.DeactivateApplication(appId) + _, err = client.Application.DeleteApplication(appId) + + require.NoError(t, err, "Deleting an application should not error") } diff --git a/tests/integration/group_test.go b/tests/integration/group_test.go index 8b896453e..079b5d834 100644 --- a/tests/integration/group_test.go +++ b/tests/integration/group_test.go @@ -274,7 +274,7 @@ func Test_group_rule_operations(t *testing.T) { _, err = client.Group.ActivateRule(groupRule.Id) require.NoError(t, err, "Should not error when activating rule") - time.Sleep(4 * time.Second) + time.Sleep(6 * time.Second) users, _, err := client.Group.ListGroupUsers(group.Id, nil) found := false for _, tmpuser := range users { diff --git a/tests/unit/client_config_test.go b/tests/unit/client_config_test.go index 1c6638f8d..da1cce2a5 100644 --- a/tests/unit/client_config_test.go +++ b/tests/unit/client_config_test.go @@ -93,6 +93,6 @@ func Test_does_not_panic_when_authorization_mode_is_valid(t *testing.T) { func Test_will_panic_if_private_key_authorization_type_with_missing_properties(t *testing.T) { assert.Panics(t, func() { - _, _ = tests.NewClient(okta.WithAuthorizationMode("PrivateKey")) + _, _ = tests.NewClient(okta.WithAuthorizationMode("PrivateKey"), okta.WithClientId("")) }, "Does not panic if private key selected with no other required options") }