Skip to content

Commit

Permalink
[Breaking] OAuth2 Authorization Server implementation, Separate OpenI…
Browse files Browse the repository at this point in the history
…D and OAuth2 configs, OAuth2 Metadata over gRPC #minor (flyteorg#168)

* wip: OAuth2 Support

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* wip

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* wip

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* tighten security of generated tokens

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Support storing form post values in auth code JWT

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* save secrets to k8s secrets

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Expose metadata endpoints over gRPC

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* trim OpenID Connect config further

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Selectively authenticate gRPC endpoints

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Support external oauth2 server and Okta Config

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* update config

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Fix nil secrets data map

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Fixed the pointer overwrite issue in oauthServer metadata (flyteorg#183)

Signed-off-by: Prafulla Mahindrakar <[email protected]>

Co-authored-by: Prafulla Mahindrakar <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Unit tests

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Unit tests

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Simplify config further and move auth package up

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Fix clusterresource Project and domain(flyteorg#167)

* Fix clusterresource Project

Signed-off-by: Anand Swaminathan <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Bump flyteidl version to pick up auth role field number fix (flyteorg#169)

Signed-off-by: Katrina Rogan <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Add option to use project name as namespace for the task pods (flyteorg#166)

* Add option to use project name as namespace for the task pods

Signed-off-by: Jeev B <[email protected]>

* rename

Signed-off-by: Jeev B <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* GetExecution performance improvements (flyteorg#171)

Signed-off-by: Katrina Rogan <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Add exists check for workflow & node executions (flyteorg#172)

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Remove legacy fetch for workflow execution inputs (flyteorg#173)

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Added release workflow (flyteorg#170)

Signed-off-by: yuvraj <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Update Flyteidl version (flyteorg#175)

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Added version in flyteadmin (flyteorg#154)

* wip: added version pkg

Signed-off-by: yuvraj <[email protected]>

* wip: resolve conflict

Signed-off-by: yuvraj <[email protected]>

* wip: added version in rpc

Signed-off-by: yuvraj <[email protected]>

* wip: small fixes

Signed-off-by: yuvraj <[email protected]>

* wip: Added panic cache in get version service

Signed-off-by: yuvraj <[email protected]>

* Added flytestdlib for version package

Signed-off-by: yuvraj <[email protected]>

* Added version service test

Signed-off-by: yuvraj <[email protected]>

* wip: added ldflags in goreleaser

Signed-off-by: yuvraj <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Propagate nesting and principal for child executions (flyteorg#177)

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Write workflow and node execution events asynchronously (flyteorg#174)

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Add sensible flyteadmin config defaults (flyteorg#179)

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Lint

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* further cleanup

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Only register authserver when auth is enabled

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Update to latest flyteidl and separate auth interfaces

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* dead code

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* PR Comments

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* merge master

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Move to authorizedUris

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Update to released flyteidl

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Fix response expiry and add unit tests

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Update go mod

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* fix unit tests that broke because of identity changes

Signed-off-by: Haytham Abuelfutuh <[email protected]>

Co-authored-by: pmahindrakar-oss <[email protected]>
Co-authored-by: Prafulla Mahindrakar <[email protected]>
Co-authored-by: Anand Swaminathan <[email protected]>
Co-authored-by: Katrina Rogan <[email protected]>
Co-authored-by: Jeev B <[email protected]>
Co-authored-by: Yuvraj <[email protected]>
Co-authored-by: Flyte Bot <[email protected]>
  • Loading branch information
8 people authored Apr 30, 2021
1 parent f1faabd commit 435f520
Show file tree
Hide file tree
Showing 73 changed files with 6,966 additions and 1,426 deletions.
212 changes: 212 additions & 0 deletions flyteadmin/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 flyteadmin/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

0 comments on commit 435f520

Please sign in to comment.