Skip to content
This repository has been archived by the owner on Oct 9, 2023. It is now read-only.

[Breaking] OAuth2 Authorization Server implementation, Separate OpenID and OAuth2 configs, OAuth2 Metadata over gRPC #minor #168

Merged
merged 42 commits into from
Apr 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
90212c2
wip: OAuth2 Support
EngHabu Mar 25, 2021
825894b
wip
EngHabu Mar 30, 2021
9ab26bc
wip
EngHabu Mar 31, 2021
c4d3900
tighten security of generated tokens
EngHabu Apr 6, 2021
6054cae
Support storing form post values in auth code JWT
EngHabu Apr 7, 2021
b9ce79d
save secrets to k8s secrets
EngHabu Apr 8, 2021
57b68a4
Expose metadata endpoints over gRPC
EngHabu Apr 11, 2021
1dba389
trim OpenID Connect config further
EngHabu Apr 11, 2021
cdd88e7
Selectively authenticate gRPC endpoints
EngHabu Apr 12, 2021
8128f54
Support external oauth2 server and Okta Config
EngHabu Apr 12, 2021
6296e5f
update config
EngHabu Apr 13, 2021
cd821d2
Fix nil secrets data map
EngHabu Apr 13, 2021
5bc62b5
Fixed the pointer overwrite issue in oauthServer metadata (#183)
pmahindrakar-oss Apr 14, 2021
bba52f8
Unit tests
EngHabu Apr 14, 2021
b9bbf7a
Unit tests
EngHabu Apr 15, 2021
de186fd
Simplify config further and move auth package up
EngHabu Apr 20, 2021
b289138
Fix clusterresource Project and domain(#167)
anandswaminathan Mar 24, 2021
b441999
Bump flyteidl version to pick up auth role field number fix (#169)
Mar 25, 2021
912d2e2
Add option to use project name as namespace for the task pods (#166)
jeevb Mar 25, 2021
1dbcf41
GetExecution performance improvements (#171)
Mar 30, 2021
3657fa2
Add exists check for workflow & node executions (#172)
Mar 31, 2021
ce645ec
Remove legacy fetch for workflow execution inputs (#173)
Mar 31, 2021
3971176
Added release workflow (#170)
yindia Apr 3, 2021
4fe0d6b
Update Flyteidl version (#175)
flyte-bot Apr 6, 2021
a0fa260
Added version in flyteadmin (#154)
yindia Apr 8, 2021
5674c28
Propagate nesting and principal for child executions (#177)
Apr 8, 2021
2b02791
Write workflow and node execution events asynchronously (#174)
Apr 9, 2021
f93e982
Add sensible flyteadmin config defaults (#179)
Apr 12, 2021
d332e6d
Lint
EngHabu Apr 20, 2021
9d63e51
further cleanup
EngHabu Apr 20, 2021
8d70f7a
Only register authserver when auth is enabled
EngHabu Apr 20, 2021
7a34866
Update to latest flyteidl and separate auth interfaces
EngHabu Apr 24, 2021
3196b10
dead code
EngHabu Apr 26, 2021
6b01a37
PR Comments
EngHabu Apr 26, 2021
ee04ea5
merge master
EngHabu Apr 26, 2021
4e97288
merge master
EngHabu Apr 26, 2021
f544a2d
Move to authorizedUris
EngHabu Apr 29, 2021
eb3c533
Update to released flyteidl
EngHabu Apr 30, 2021
da6740f
Fix response expiry and add unit tests
EngHabu Apr 30, 2021
5282c67
Update go mod
EngHabu Apr 30, 2021
5e51b86
Merge branch 'master' into oauth2
EngHabu Apr 30, 2021
03fde57
fix unit tests that broke because of identity changes
EngHabu Apr 30, 2021
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
212 changes: 212 additions & 0 deletions auth/auth_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Contains types needed to start up a standalone OAuth2 Authorization Server or delegate authentication to an external
// provider. It supports OpenId connect for user authentication.
package auth

import (
"context"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"

"github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/service"
"github.com/flyteorg/flyteplugins/go/tasks/pluginmachinery/core"

"github.com/coreos/go-oidc"
"github.com/flyteorg/flyteadmin/auth/config"
"github.com/flyteorg/flyteadmin/auth/interfaces"
"github.com/flyteorg/flytestdlib/errors"
"github.com/flyteorg/flytestdlib/logger"
"golang.org/x/oauth2"
)

const (
IdpConnectionTimeout = 10 * time.Second

ErrauthCtx errors.ErrorCode = "AUTH_CONTEXT_SETUP_FAILED"
ErrConfigFileRead errors.ErrorCode = "CONFIG_OPTION_FILE_READ_FAILED"
)

var (
callbackRelativeURL = config.MustParseURL("/callback")
rootRelativeURL = config.MustParseURL("/")
)

// Please see the comment on the corresponding AuthenticationContext for more information.
type Context struct {
oauth2Client *oauth2.Config
cookieManager interfaces.CookieHandler
oidcProvider *oidc.Provider
options *config.Config
oauth2Provider interfaces.OAuth2Provider
oauth2ResourceServer interfaces.OAuth2ResourceServer
authServiceImpl service.AuthMetadataServiceServer
identityServiceIml service.IdentityServiceServer

userInfoURL *url.URL
oauth2MetadataURL *url.URL
oidcMetadataURL *url.URL
httpClient *http.Client
}

func (c Context) OAuth2Provider() interfaces.OAuth2Provider {
return c.oauth2Provider
}

func (c Context) OAuth2ClientConfig(requestURL *url.URL) *oauth2.Config {
if requestURL == nil || strings.HasPrefix(c.oauth2Client.RedirectURL, requestURL.ResolveReference(rootRelativeURL).String()) {
return c.oauth2Client
}

return &oauth2.Config{
RedirectURL: requestURL.ResolveReference(callbackRelativeURL).String(),
ClientID: c.oauth2Client.ClientID,
ClientSecret: c.oauth2Client.ClientSecret,
Scopes: c.oauth2Client.Scopes,
Endpoint: c.oauth2Client.Endpoint,
}
}

func (c Context) OidcProvider() *oidc.Provider {
return c.oidcProvider
}

func (c Context) CookieManager() interfaces.CookieHandler {
return c.cookieManager
}

func (c Context) Options() *config.Config {
return c.options
}

func (c Context) GetUserInfoURL() *url.URL {
return c.userInfoURL
}

func (c Context) GetHTTPClient() *http.Client {
return c.httpClient
}

func (c Context) GetOAuth2MetadataURL() *url.URL {
return c.oauth2MetadataURL
}

func (c Context) GetOIdCMetadataURL() *url.URL {
return c.oidcMetadataURL
}

func (c Context) AuthMetadataService() service.AuthMetadataServiceServer {
return c.authServiceImpl
}

func (c Context) IdentityService() service.IdentityServiceServer {
return c.identityServiceIml
}

func (c Context) OAuth2ResourceServer() interfaces.OAuth2ResourceServer {
return c.oauth2ResourceServer
}
func NewAuthenticationContext(ctx context.Context, sm core.SecretManager, oauth2Provider interfaces.OAuth2Provider,
oauth2ResourceServer interfaces.OAuth2ResourceServer, authMetadataService service.AuthMetadataServiceServer,
identityService service.IdentityServiceServer, options *config.Config) (Context, error) {

// Construct the cookie manager object.
hashKeyBase64, err := sm.Get(ctx, options.UserAuth.CookieHashKeySecretName)
if err != nil {
return Context{}, errors.Wrapf(ErrConfigFileRead, err, "Could not read hash key file")
}

blockKeyBase64, err := sm.Get(ctx, options.UserAuth.CookieBlockKeySecretName)
if err != nil {
return Context{}, errors.Wrapf(ErrConfigFileRead, err, "Could not read hash key file")
}

cookieManager, err := NewCookieManager(ctx, hashKeyBase64, blockKeyBase64)
if err != nil {
logger.Errorf(ctx, "Error creating cookie manager %s", err)
return Context{}, errors.Wrapf(ErrauthCtx, err, "Error creating cookie manager")
}

// Construct an http client for interacting with the IDP if necessary.
httpClient := &http.Client{
Timeout: IdpConnectionTimeout,
}

// Construct an oidc Provider, which needs its own http Client.
oidcCtx := oidc.ClientContext(ctx, httpClient)
baseURL := options.UserAuth.OpenID.BaseURL.String()
provider, err := oidc.NewProvider(oidcCtx, baseURL)
if err != nil {
return Context{}, errors.Wrapf(ErrauthCtx, err, "Error creating oidc provider w/ issuer [%v]", baseURL)
}

// Construct the golang OAuth2 library's own internal configuration object from this package's config
oauth2Config, err := GetOAuth2ClientConfig(ctx, options.UserAuth.OpenID, provider.Endpoint(), sm)
if err != nil {
return Context{}, errors.Wrapf(ErrauthCtx, err, "Error creating OAuth2 library configuration")
}

logger.Infof(ctx, "Base IDP URL is %s", options.UserAuth.OpenID.BaseURL)

oauth2MetadataURL, err := url.Parse(OAuth2MetadataEndpoint)
if err != nil {
logger.Errorf(ctx, "Error parsing oauth2 metadata URL %s", err)
return Context{}, errors.Wrapf(ErrauthCtx, err, "Error parsing metadata URL")
}

logger.Infof(ctx, "Metadata endpoint is %s", oauth2MetadataURL)

oidcMetadataURL, err := url.Parse(OIdCMetadataEndpoint)
if err != nil {
logger.Errorf(ctx, "Error parsing oidc metadata URL %s", err)
return Context{}, errors.Wrapf(ErrauthCtx, err, "Error parsing metadata URL")
}

logger.Infof(ctx, "Metadata endpoint is %s", oidcMetadataURL)

authCtx := Context{
options: options,
oidcMetadataURL: oidcMetadataURL,
oauth2MetadataURL: oauth2MetadataURL,
oauth2Client: &oauth2Config,
oidcProvider: provider,
httpClient: httpClient,
cookieManager: cookieManager,
oauth2Provider: oauth2Provider,
oauth2ResourceServer: oauth2ResourceServer,
}

authCtx.authServiceImpl = authMetadataService
authCtx.identityServiceIml = identityService

return authCtx, nil
}

// This creates a oauth2 library config object, with values from the Flyte Admin config
func GetOAuth2ClientConfig(ctx context.Context, options config.OpenIDOptions, providerEndpoints oauth2.Endpoint, sm core.SecretManager) (cfg oauth2.Config, err error) {
var secret string
if len(options.DeprecatedClientSecretFile) > 0 {
secretBytes, err := ioutil.ReadFile(options.DeprecatedClientSecretFile)
if err != nil {
return oauth2.Config{}, err
}

secret = string(secretBytes)
} else {
secret, err = sm.Get(ctx, options.ClientSecretName)
if err != nil {
return oauth2.Config{}, err
}
}

secret = strings.TrimSuffix(secret, "\n")

return oauth2.Config{
RedirectURL: callbackRelativeURL.String(),
ClientID: options.ClientID,
ClientSecret: secret,
Scopes: options.Scopes,
Endpoint: providerEndpoints,
}, nil
}
133 changes: 133 additions & 0 deletions auth/authzserver/authorize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package authzserver

import (
"fmt"
"log"
"net/http"
"time"

"github.com/flyteorg/flyteadmin/auth"
"github.com/ory/fosite"

"github.com/flyteorg/flyteadmin/auth/interfaces"
"github.com/flyteorg/flytestdlib/logger"
)

const (
requestedScopePrefix = "f."
accessTokenScope = "access_token"
refreshTokenScope = "offline"
)

func getAuthEndpoint(authCtx interfaces.AuthenticationContext) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
authEndpoint(authCtx, writer, request)
}
}

func getAuthCallbackEndpoint(authCtx interfaces.AuthenticationContext) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
authCallbackEndpoint(authCtx, writer, request)
}
}

// authCallbackEndpoint is the endpoint that gets called after the user-auth flow finishes. It retrieves the original
// /authorize request and issues an auth_code in response.
func authCallbackEndpoint(authCtx interfaces.AuthenticationContext, rw http.ResponseWriter, req *http.Request) {
issuer := GetIssuer(req.Context(), req, authCtx.Options())

// This context will be passed to all methods.
ctx := req.Context()
oauth2Provider := authCtx.OAuth2Provider()

// Get the user's identity
identityContext, err := auth.IdentityContextFromRequest(ctx, req, authCtx)
if err != nil {
logger.Infof(ctx, "Failed to acquire user identity from request: %+v", err)
oauth2Provider.WriteAuthorizeError(rw, fosite.NewAuthorizeRequest(), err)
return
}

// Get latest user's info either from identity or by making a UserInfo() call to the original
userInfo, err := auth.QueryUserInfo(ctx, identityContext, req, authCtx)
if err != nil {
err = fmt.Errorf("failed to query user info. Error: %w", err)
http.Error(rw, err.Error(), http.StatusUnauthorized)
return
}

// Rehydrate the original auth code request
arURL, err := authCtx.CookieManager().RetrieveAuthCodeRequest(ctx, req)
if err != nil {
logger.Infof(ctx, "Error occurred in NewAuthorizeRequest: %+v", err)
oauth2Provider.WriteAuthorizeError(rw, fosite.NewAuthorizeRequest(), err)
return
}

arReq, err := http.NewRequest(http.MethodGet, arURL, nil)
if err != nil {
logger.Infof(ctx, "Error occurred in NewAuthorizeRequest: %+v", err)
oauth2Provider.WriteAuthorizeError(rw, fosite.NewAuthorizeRequest(), err)
return
}

ar, err := oauth2Provider.NewAuthorizeRequest(ctx, arReq)
if err != nil {
logger.Infof(ctx, "Error occurred in NewAuthorizeRequest: %+v", err)
oauth2Provider.WriteAuthorizeError(rw, ar, err)
return
}

// TODO: Ideally this is where we show users a consent form.

// let's see what scopes the user gave consent to
for _, scope := range req.PostForm["scopes"] {
ar.GrantScope(scope)
}

// Now that the user is authorized, we set up a session:
mySessionData := oauth2Provider.NewJWTSessionToken(identityContext.UserID(), ar.GetClient().GetID(), issuer, issuer, userInfo)
mySessionData.JWTClaims.ExpiresAt = time.Now().Add(authCtx.Options().AppAuth.SelfAuthServer.AccessTokenLifespan.Duration)
mySessionData.SetExpiresAt(fosite.AuthorizeCode, time.Now().Add(authCtx.Options().AppAuth.SelfAuthServer.AuthorizationCodeLifespan.Duration))
mySessionData.SetExpiresAt(fosite.AccessToken, time.Now().Add(authCtx.Options().AppAuth.SelfAuthServer.AccessTokenLifespan.Duration))
mySessionData.SetExpiresAt(fosite.RefreshToken, time.Now().Add(authCtx.Options().AppAuth.SelfAuthServer.RefreshTokenLifespan.Duration))

// Now we need to get a response. This is the place where the AuthorizeEndpointHandlers kick in and start processing the request.
// NewAuthorizeResponse is capable of running multiple response type handlers.
response, err := oauth2Provider.NewAuthorizeResponse(ctx, ar, mySessionData)
if err != nil {
log.Printf("Error occurred in NewAuthorizeResponse: %+v", err)
oauth2Provider.WriteAuthorizeError(rw, ar, err)
return
}

// Last but not least, send the response!
oauth2Provider.WriteAuthorizeResponse(rw, ar, response)
}

// Get the /authorize endpoint handler that is supposed to be invoked in the browser for the user to log in and consent.
func authEndpoint(authCtx interfaces.AuthenticationContext, rw http.ResponseWriter, req *http.Request) {
// This context will be passed to all methods.
ctx := req.Context()

oauth2Provider := authCtx.OAuth2Provider()

// Let's create an AuthorizeRequest object!
// It will analyze the request and extract important information like scopes, response type and others.
ar, err := oauth2Provider.NewAuthorizeRequest(ctx, req)
if err != nil {
logger.Infof(ctx, "Error occurred in NewAuthorizeRequest: %+v", err)
oauth2Provider.WriteAuthorizeError(rw, ar, err)
return
}

err = authCtx.CookieManager().SetAuthCodeCookie(ctx, rw, req.URL.String())
if err != nil {
logger.Infof(ctx, "Error occurred in NewAuthorizeRequest: %+v", err)
oauth2Provider.WriteAuthorizeError(rw, ar, err)
return
}

redirectURL := fmt.Sprintf("/login?redirect_url=%v", authorizeCallbackRelativeURL.String())
http.Redirect(rw, req, redirectURL, http.StatusTemporaryRedirect)
}
Loading