Skip to content

sso: okta provider MVP #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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