Skip to content

Commit

Permalink
okta sso provider mvp
Browse files Browse the repository at this point in the history
validateToken

validateToken, groups and email

tidy validateToken

UserInfo

UserInfo continued

Might be worth splitting this function up so it can serve different
purposes if required in the future. However for now it does the job.

some cleanup and fix group validation

change default to google

cleaning up

updating comments

removng BaseURL from provider for now

removing BaseURL from options and modifying UserInfoURL and RevokeURL

fix tags

removing BaseURL from provider_data

add offline_access for refresh token

tidying

add empty line back in

separate validateToken into respective provider packages

review changes

change from oktapreview to okta

rename UserInfo to GetUserProfile

removing validateToken func and tests

extra oktapreview -> okta

add initial test file

skipping while debugging

fixing TestValidateEndpoint tests

removing skip

check response contents to properly validate

removing debug output

changes from review, and adding relevant test case

use test logger instead of stdlib logger

use %q instead of %s to quote formatted output

moving formatting to options.go, and reducing repetition

move logic to more relevant function

set entire org url rather than just org name

fixing test
  • Loading branch information
Jusshersmith committed Apr 24, 2019
1 parent d0e8e03 commit 5edbfbd
Show file tree
Hide file tree
Showing 9 changed files with 990 additions and 111 deletions.
33 changes: 29 additions & 4 deletions internal/auth/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
)

// Options are config options that can be set by environment variables
// RedirectURL string - the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\
// ClientID - string - the OAuth ClientID ie "123456.apps.googleusercontent.com"
// RedirectURL - string - the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\
// ClientID - string - the OAuth ClientID ie "123456.apps.googleusercontent.com"
// ClientSecret string - the OAuth Client Secret
// OrgName - string - if using Okta as the provider, the Okta domain to use
// ProxyClientID - string - the client id that matches the sso proxy client id
// ProxyClientSecret - string - the client secret that matches the sso proxy client secret
// Host - string - The host that is in the header that is required on incoming requests
Expand All @@ -42,8 +43,10 @@ import (
// PassUserHeaders - bool (default true) - pass X-Forwarded-User and X-Forwarded-Email information to upstream
// SetXAuthRequest - set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)
// Provider - provider name
// ProviderServerID - string - if using Okta as the provider, the authorisation server ID (defaults to 'default')
// SignInURL - provider sign in endpoint
// RedeemURL - provider token redemption endpoint
// RevokeURL - provider revoke token endpoint
// ProfileURL - provider profile access endpoint
// ValidateURL - access token validation endpoint
// Scope - Oauth scope specification
Expand All @@ -68,6 +71,8 @@ type Options struct {
GoogleAdminEmail string `envconfig:"GOOGLE_ADMIN_EMAIL"`
GoogleServiceAccountJSON string `envconfig:"GOOGLE_SERVICE_ACCOUNT_JSON"`

OrgURL string `envconfig:"OKTA_ORG_URL"`

Footer string `envconfig:"FOOTER"`

CookieName string
Expand All @@ -93,9 +98,12 @@ type Options struct {
SetXAuthRequest bool `envconfig:"SET_XAUTHREQUEST" default:"false"`

// These options allow for other providers besides Google, with potential overrides.
Provider string `envconfig:"PROVIDER" default:"google"`
Provider string `envconfig:"PROVIDER" default:"google"`
ProviderServerID string `envconfig:"PROVIDER_SERVER_ID" default:"default"`

SignInURL string `envconfig:"SIGNIN_URL"`
RedeemURL string `envconfig:"REDEEM_URL"`
RevokeURL string `envconfig:"REVOKE_URL"`
ProfileURL string `envconfig:"PROFILE_URL"`
ValidateURL string `envconfig:"VALIDATE_URL"`
Scope string `envconfig:"SCOPE"`
Expand Down Expand Up @@ -172,6 +180,13 @@ func (o *Options) Validate() error {
msgs = append(msgs, "missing setting: required-host-header")
}

if len(o.OrgURL) > 0 {
o.OrgURL = strings.Trim(o.OrgURL, `"`)
}
if len(o.ProviderServerID) > 0 {
o.ProviderServerID = strings.Trim(o.ProviderServerID, `"`)
}

o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)

msgs = validateEndpoints(o, msgs)
Expand Down Expand Up @@ -222,6 +237,7 @@ func (o *Options) Validate() error {
func validateEndpoints(o *Options, msgs []string) []string {
_, msgs = parseURL(o.SignInURL, "signin", msgs)
_, msgs = parseURL(o.RedeemURL, "redeem", msgs)
_, msgs = parseURL(o.RevokeURL, "revoke", msgs)
_, msgs = parseURL(o.ProfileURL, "profile", msgs)
_, msgs = parseURL(o.ValidateURL, "validate", msgs)

Expand All @@ -246,13 +262,16 @@ func newProvider(o *Options) (providers.Provider, error) {
}

var err error

if p.SignInURL, err = url.Parse(o.SignInURL); err != nil {
return nil, err
}
if p.RedeemURL, err = url.Parse(o.RedeemURL); err != nil {
return nil, err
}
p.RevokeURL = &url.URL{}
if p.RevokeURL, err = url.Parse(o.RevokeURL); err != nil {
return nil, err
}
if p.ProfileURL, err = url.Parse(o.ProfileURL); err != nil {
return nil, err
}
Expand All @@ -277,6 +296,12 @@ func newProvider(o *Options) (providers.Provider, error) {
googleProvider.GroupsCache = cache
o.GroupsCacheStopFunc = cache.Stop
singleFlightProvider = providers.NewSingleFlightProvider(googleProvider)
case providers.OktaProviderName:
oktaProvider, err := providers.NewOktaProvider(p, o.OrgURL, o.ProviderServerID)
if err != nil {
return nil, err
}
singleFlightProvider = providers.NewSingleFlightProvider(oktaProvider)
default:
return nil, fmt.Errorf("unimplemented provider: %q", o.Provider)
}
Expand Down
18 changes: 16 additions & 2 deletions internal/auth/providers/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,21 @@ func (p *GoogleProvider) SetStatsdClient(statsdClient *statsd.Client) {

// ValidateSessionState attempts to validate the session state's access token.
func (p *GoogleProvider) ValidateSessionState(s *sessions.SessionState) bool {
return validateToken(p, s.AccessToken, nil)
if s.AccessToken == "" {
return false
}

var endpoint url.URL
endpoint = *p.ValidateURL
q := endpoint.Query()
q.Add("access_token", s.AccessToken)
endpoint.RawQuery = q.Encode()

err := p.googleRequest("POST", endpoint.String(), nil, []string{"action:validate"}, nil)
if err != nil {
return false
}
return true
}

// GetSignInURL returns the sign in url with typical oauth parameters
Expand Down Expand Up @@ -186,7 +200,7 @@ func (p *GoogleProvider) googleRequest(method, endpoint string, params url.Value

if resp.StatusCode != http.StatusOK {
p.StatsdClient.Incr("provider.error", tags, 1.0)
logger.WithHTTPStatus(resp.StatusCode).WithEndpoint(endpoint).WithResponseBody(
logger.WithHTTPStatus(resp.StatusCode).WithEndpoint(stripToken(endpoint)).WithResponseBody(
respBody).Info()
switch resp.StatusCode {
case 400:
Expand Down
51 changes: 51 additions & 0 deletions internal/auth/providers/google_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,54 @@ func TestValidateGroupMembers(t *testing.T) {
})
}
}

func TestGoogleProviderValidateSession(t *testing.T) {
testCases := []struct {
name string
resp oktaProviderValidateSessionResponse
httpStatus int
expectedError bool
sessionState *sessions.SessionState
}{
{
name: "valid session state",
sessionState: &sessions.SessionState{
AccessToken: "a1234",
},
httpStatus: http.StatusOK,
expectedError: false,
},
{
name: "invalid session state",
sessionState: &sessions.SessionState{
AccessToken: "a1234",
},
httpStatus: http.StatusBadRequest,
expectedError: true,
},
{
name: "missing access token",
sessionState: &sessions.SessionState{},
expectedError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := newGoogleProvider(nil)
body, err := json.Marshal(tc.resp)
testutil.Equal(t, nil, err)
var server *httptest.Server
p.ValidateURL, server = newProviderServer(body, tc.httpStatus)
defer server.Close()

resp := p.ValidateSessionState(tc.sessionState)
if tc.expectedError && resp {
t.Errorf("expected false but returned as true")
}
if !tc.expectedError && !resp {
t.Errorf("expected true but returned as false")
}
})
}
}
40 changes: 0 additions & 40 deletions internal/auth/providers/internal_util.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package providers

import (
"io/ioutil"
"net/http"
"net/url"

log "github.com/buzzfeed/sso/internal/pkg/logging"
Expand Down Expand Up @@ -45,41 +43,3 @@ func stripParam(param, endpoint string) string {

return endpoint
}

// validateToken returns true if token is valid
func validateToken(p Provider, accessToken string, header http.Header) bool {
logger := log.NewLogEntry()

if accessToken == "" || p.Data().ValidateURL == nil {
return false
}
endpoint := p.Data().ValidateURL.String()
if len(header) == 0 {
params := url.Values{"access_token": {accessToken}}
endpoint = endpoint + "?" + params.Encode()
}

req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
logger.Error(err, "token validation request failed")
return false
}
req.Header = header

resp, err := httpClient.Do(req)
if err != nil {
logger.Error(err, "token validation request failed")
return false
}

body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
logger.WithHTTPStatus(resp.StatusCode).WithEndpoint(stripToken(endpoint)).Info(
"validateToken response")

if resp.StatusCode == 200 {
return true
}
logger.WithResponseBody(body).Info("validateToken failed")
return false
}
64 changes: 0 additions & 64 deletions internal/auth/providers/internal_util_test.go
Original file line number Diff line number Diff line change
@@ -1,75 +1,11 @@
package providers

import (
"net/http"
"net/url"
"testing"

"github.com/buzzfeed/sso/internal/pkg/testutil"
)

func TestValidateToken(t *testing.T) {
testCases := []struct {
name string
validToken bool
statusCode int
emptyValidateURL bool
accessToken string
expectedValid bool
}{
{
name: "valid token",
accessToken: "foo",
expectedValid: true,
statusCode: http.StatusOK,
},

{
name: "token in headers",
expectedValid: true,
accessToken: "foo",
statusCode: http.StatusOK,
},
{
name: "empty accessToken",
validToken: true,
expectedValid: false,
statusCode: http.StatusOK,
},
{
name: "no validate url",
expectedValid: false,
statusCode: http.StatusOK,
emptyValidateURL: true,
},
{
name: "invalid token",
accessToken: "foo",
expectedValid: false,
statusCode: http.StatusUnauthorized,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testProvider := NewTestProvider(&url.URL{})
testProvider.ValidToken = true
validateURL, server := newProviderServer(nil, tc.statusCode)
defer server.Close()
testProvider.ValidateURL = validateURL
if tc.emptyValidateURL {
testProvider.ValidateURL = nil
}
header := http.Header{}
header.Set("Authorization", "foo")
isValid := validateToken(testProvider, tc.accessToken, header)
if isValid != tc.expectedValid {
t.Errorf("expected valid to be %v but was %v", tc.expectedValid, isValid)
}

})
}
}

func TestStripTokenNotPresent(t *testing.T) {
test := "http://local.test/api/test?a=1&b=2"
testutil.Equal(t, test, stripToken(test))
Expand Down
Loading

0 comments on commit 5edbfbd

Please sign in to comment.