Skip to content

Commit

Permalink
Merge 490a0b5 into f3337bd
Browse files Browse the repository at this point in the history
  • Loading branch information
AlinaGoaga committed Feb 21, 2022
2 parents f3337bd + 490a0b5 commit 9c3b1d5
Show file tree
Hide file tree
Showing 16 changed files with 1,387 additions and 206 deletions.
7 changes: 5 additions & 2 deletions .proxyrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"/v1": {
"target": "http://localhost:9000/",
}
"target": "http://localhost:9001/",
},
"/oauth2": {
"target": "http://localhost:9001/"
},
}
35 changes: 16 additions & 19 deletions cmd/gitops/ui/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ type Options struct {
// OIDCAuthenticationOptions contains the OIDC authentication options for the
// `ui run` command.
type OIDCAuthenticationOptions struct {
IssuerURL string
ClientID string
ClientSecret string
RedirectURL string
CookieDuration time.Duration
IssuerURL string
ClientID string
ClientSecret string
RedirectURL string
TokenDuration time.Duration
}

var options Options
Expand Down Expand Up @@ -83,7 +83,7 @@ func NewCommand() *cobra.Command {
cmd.Flags().StringVar(&options.OIDC.ClientID, "oidc-client-id", "", "The client ID for the OpenID Connect client")
cmd.Flags().StringVar(&options.OIDC.ClientSecret, "oidc-client-secret", "", "The client secret to use with OpenID Connect issuer")
cmd.Flags().StringVar(&options.OIDC.RedirectURL, "oidc-redirect-url", "", "The OAuth2 redirect URL")
cmd.Flags().DurationVar(&options.OIDC.CookieDuration, "oidc-cookie-duration", time.Hour, "The duration of the ID token cookie. It should be set in the format: number + time unit (s,m,h) e.g., 20m")
cmd.Flags().DurationVar(&options.OIDC.TokenDuration, "oidc-token-duration", time.Hour, "The duration of the ID token. It should be set in the format: number + time unit (s,m,h) e.g., 20m")
}

return cmd
Expand Down Expand Up @@ -189,29 +189,26 @@ func runCmd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid issuer URL: %w", err)
}

redirectURL, err := url.Parse(options.OIDC.RedirectURL)
_, err = url.Parse(options.OIDC.RedirectURL)
if err != nil {
return fmt.Errorf("invalid redirect URL: %w", err)
}

var oidcIssueSecureCookies bool
if redirectURL.Scheme == "https" {
oidcIssueSecureCookies = true
tsv, err := auth.NewHMACTokenSignerVerifier(options.OIDC.TokenDuration)
if err != nil {
return fmt.Errorf("could not create HMAC token signer: %w", err)
}

srv, err := auth.NewAuthServer(cmd.Context(), appConfig.Logger, http.DefaultClient,
auth.AuthConfig{
OIDCConfig: auth.OIDCConfig{
IssuerURL: options.OIDC.IssuerURL,
ClientID: options.OIDC.ClientID,
ClientSecret: options.OIDC.ClientSecret,
RedirectURL: options.OIDC.RedirectURL,
},
CookieConfig: auth.CookieConfig{
CookieDuration: options.OIDC.CookieDuration,
IssueSecureCookies: oidcIssueSecureCookies,
IssuerURL: options.OIDC.IssuerURL,
ClientID: options.OIDC.ClientID,
ClientSecret: options.OIDC.ClientSecret,
RedirectURL: options.OIDC.RedirectURL,
TokenDuration: options.OIDC.TokenDuration,
},
},
}, rawClient, tsv,
)
if err != nil {
return fmt.Errorf("could not create auth server: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ require (
github.com/gofrs/flock v0.8.1
github.com/google/uuid v1.3.0
github.com/oauth2-proxy/mockoidc v0.0.0-20210703044157-382d3faf2671
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
gopkg.in/square/go-jose.v2 v2.5.1
gopkg.in/yaml.v2 v2.4.0
)
Expand Down Expand Up @@ -211,7 +212,6 @@ require (
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
"peerDependencies": {
"lodash": "^4.17.21",
"luxon": "^1.27.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-toastify": "^7.0.4",
"react": "^17.0.2",
"styled-components": "^5.3.0"
},
"dependencies": {
Expand Down
47 changes: 12 additions & 35 deletions pkg/server/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
)

Expand All @@ -30,7 +28,9 @@ const (
// This route is called by the OIDC Provider in order to pass back state after
// the authentication flow completes.
func RegisterAuthServer(mux *http.ServeMux, prefix string, srv *AuthServer) {
mux.Handle(prefix+"/callback", srv)
mux.Handle(prefix+"/callback", srv.Callback())
mux.Handle(prefix+"/sign_in", srv.SignIn())
mux.Handle(prefix+"/userinfo", srv.UserInfo())
}

type principalCtxKey struct{}
Expand Down Expand Up @@ -60,10 +60,13 @@ func WithPrincipal(ctx context.Context, p *UserPrincipal) context.Context {
//
// Unauthorized requests will be denied with a 401 status code.
func WithAPIAuth(next http.Handler, srv *AuthServer) http.Handler {
adminAuth := NewJWTAdminCookiePrincipalGetter(srv.logger, srv.tokenSignerVerifier, IDTokenCookieName)
cookieAuth := NewJWTCookiePrincipalGetter(srv.logger,
srv.verifier(), IDTokenCookieName)
headerAuth := NewJWTAuthorizationHeaderPrincipalGetter(srv.logger, srv.verifier())
multi := MultiAuthPrincipal{cookieAuth, headerAuth}
multi := MultiAuthPrincipal{
adminAuth,
cookieAuth, headerAuth}

return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
principal, err := multi.Principal(r)
Expand All @@ -86,10 +89,13 @@ func WithAPIAuth(next http.Handler, srv *AuthServer) http.Handler {
// It is meant to be used with routes that serve HTML content,
// not API routes.
func WithWebAuth(next http.Handler, srv *AuthServer) http.Handler {
adminAuth := NewJWTAdminCookiePrincipalGetter(srv.logger, srv.tokenSignerVerifier, IDTokenCookieName)
cookieAuth := NewJWTCookiePrincipalGetter(srv.logger,
srv.verifier(), IDTokenCookieName)
headerAuth := NewJWTAuthorizationHeaderPrincipalGetter(srv.logger, srv.verifier())
multi := MultiAuthPrincipal{cookieAuth, headerAuth}
multi := MultiAuthPrincipal{
adminAuth,
cookieAuth, headerAuth}

return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
principal, err := multi.Principal(r)
Expand All @@ -98,43 +104,14 @@ func WithWebAuth(next http.Handler, srv *AuthServer) http.Handler {
}

if principal == nil || err != nil {
startAuthFlow(rw, r, srv)
srv.startAuthFlow(rw, r)
return
}

next.ServeHTTP(rw, r.Clone(WithPrincipal(r.Context(), principal)))
})
}

func startAuthFlow(rw http.ResponseWriter, r *http.Request, srv *AuthServer) {
nonce, err := generateNonce()
if err != nil {
http.Error(rw, fmt.Sprintf("failed to generate nonce: %v", err), http.StatusInternalServerError)
return
}

b, err := json.Marshal(SessionState{
Nonce: nonce,
ReturnURL: r.URL.String(),
})
if err != nil {
http.Error(rw, fmt.Sprintf("failed to marshal state to JSON: %v", err), http.StatusInternalServerError)
return
}

state := base64.StdEncoding.EncodeToString(b)

var scopes []string
// "openid", "offline_access", "email" and "groups" scopes added by default
scopes = append(scopes, scopeProfile)
authCodeUrl := srv.oauth2Config(scopes).AuthCodeURL(state)

// Issue state cookie
http.SetCookie(rw, srv.createCookie(StateCookieName, state))

http.Redirect(rw, r, authCodeUrl, http.StatusSeeOther)
}

func generateNonce() (string, error) {
b := make([]byte, 32)

Expand Down
43 changes: 25 additions & 18 deletions pkg/server/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/go-logr/logr"
"github.com/oauth2-proxy/mockoidc"
"github.com/weaveworks/weave-gitops/pkg/server/auth"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestWithAPIAuthReturns401ForUnauthenticatedRequests(t *testing.T) {
Expand All @@ -30,20 +31,23 @@ func TestWithAPIAuthReturns401ForUnauthenticatedRequests(t *testing.T) {

fake := m.Config()
mux := http.NewServeMux()
fakeKubernetesClient := ctrlclient.NewClientBuilder().Build()

tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute)
if err != nil {
t.Errorf("failed to create HMAC signer: %v", err)
}

srv, err := auth.NewAuthServer(ctx, logr.Discard(), http.DefaultClient,
auth.AuthConfig{
auth.OIDCConfig{
IssuerURL: fake.Issuer,
ClientID: fake.ClientID,
ClientSecret: fake.ClientSecret,
RedirectURL: "",
},
auth.CookieConfig{
CookieDuration: 20 * time.Minute,
IssueSecureCookies: false,
IssuerURL: fake.Issuer,
ClientID: fake.ClientID,
ClientSecret: fake.ClientSecret,
RedirectURL: "",
TokenDuration: 20 * time.Minute,
},
})
}, fakeKubernetesClient, tokenSignerVerifier)
if err != nil {
t.Error("failed to create auth config")
}
Expand Down Expand Up @@ -79,20 +83,23 @@ func TestWithWebAuthRedirectsToOIDCIssuerForUnauthenticatedRequests(t *testing.T

fake := m.Config()
mux := http.NewServeMux()
fakeKubernetesClient := ctrlclient.NewClientBuilder().Build()

tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute)
if err != nil {
t.Errorf("failed to create HMAC signer: %v", err)
}

srv, err := auth.NewAuthServer(ctx, logr.Discard(), http.DefaultClient,
auth.AuthConfig{
auth.OIDCConfig{
IssuerURL: fake.Issuer,
ClientID: fake.ClientID,
ClientSecret: fake.ClientSecret,
RedirectURL: "",
},
auth.CookieConfig{
CookieDuration: 20 * time.Minute,
IssueSecureCookies: false,
IssuerURL: fake.Issuer,
ClientID: fake.ClientID,
ClientSecret: fake.ClientSecret,
RedirectURL: "",
TokenDuration: 20 * time.Minute,
},
})
}, fakeKubernetesClient, tokenSignerVerifier)
if err != nil {
t.Error("failed to create auth config")
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/server/auth/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,40 @@ func parseJWTToken(ctx context.Context, verifier *oidc.IDTokenVerifier, rawIDTok
return &UserPrincipal{ID: claims.Email, Groups: claims.Groups}, nil
}

type JWTAdminCookiePrincipalGetter struct {
log logr.Logger
verifier TokenSignerVerifier
cookieName string
}

func NewJWTAdminCookiePrincipalGetter(log logr.Logger, verifier TokenSignerVerifier, cookieName string) PrincipalGetter {
return &JWTAdminCookiePrincipalGetter{
log: log,
verifier: verifier,
cookieName: cookieName,
}
}

func (pg *JWTAdminCookiePrincipalGetter) Principal(r *http.Request) (*UserPrincipal, error) {
pg.log.Info("attempt to read token from cookie")

cookie, err := r.Cookie(pg.cookieName)
if err == http.ErrNoCookie {
return nil, nil
}

return parseJWTAdminToken(pg.verifier, cookie.Value)
}

func parseJWTAdminToken(verifier TokenSignerVerifier, rawIDToken string) (*UserPrincipal, error) {
claims, err := verifier.Verify(rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify JWT token: %w", err)
}

return &UserPrincipal{ID: claims.Subject, Groups: []string{}}, nil
}

// MultiAuthPrincipal looks for a principal in an array of principal getters and
// if it finds an error or a principal it returns, otherwise it returns (nil,nil).
type MultiAuthPrincipal []PrincipalGetter
Expand Down
Loading

0 comments on commit 9c3b1d5

Please sign in to comment.