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

Add support for Epinio's Dex Auth Provider #9

Merged
merged 6 commits into from
Feb 13, 2023
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ npm run build-backend
```
cd src/jetstream
EPINIO_API_URL=<epinio API URL> /
EPINIO_API_URL=<epinio http/s API URL> /
EPINIO_WSS_URL=<epinio wss API URL> /
EPINIO_API_SKIP_SSL=<true|false> /
EPINIO_UI_URL=https://localhost:8005
EPINIO_VERSION=dev /
AUTH_ENDPOINT_TYPE=epinio /
./jetstream
Expand Down
11 changes: 8 additions & 3 deletions src/jetstream/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,13 @@ func TestLoginToCNSI(t *testing.T) {
})

_, _, ctx, pp, db, mock := setupHTTPTest(req)
ctx.Set("rancher_username", "admin")
ctx.Set("rancher_password", "changeme")

tr := &interfaces.TokenRecord{
AuthType: interfaces.AuthTypeHttpBasic,
AuthToken: "aaa",
RefreshToken: "aaa",
}
ctx.Set("token", tr)

defer db.Close()

Expand Down Expand Up @@ -814,7 +819,7 @@ func TestVerifySession(t *testing.T) {
func TestVerifySessionNoDate(t *testing.T) {
t.Parallel()

Convey("Test verify sesson without date", t, func() {
Convey("Test verify session without date", t, func() {

req := setupMockReq("GET", "", map[string]string{
"username": "admin",
Expand Down
155 changes: 118 additions & 37 deletions src/jetstream/authepinio.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand All @@ -13,8 +14,8 @@ import (

"github.com/labstack/echo/v4"

eInterfaces "github.com/epinio/ui-backend/src/jetstream/plugins/epinio/interfaces"
"github.com/epinio/ui-backend/src/jetstream/plugins/epinio/rancherproxy"
epinio_utils "github.com/epinio/ui-backend/src/jetstream/plugins/epinio/utils"

"github.com/epinio/ui-backend/src/jetstream/repository/interfaces"
)
Expand All @@ -41,8 +42,19 @@ func (a *epinioAuth) Login(c echo.Context) error {
return err
}

authType := c.Get("auth_type").(string)

var userGUID, username string
var err error

switch authType {
case "local":
userGUID, username, err = a.epinioLocalLogin(c)
case "oidc":
userGUID, username, err = a.epinioOIDCLogin(c)
}

// Perform the login and fetch session values if successful
userGUID, username, err := a.epinioLogin(c)

if err != nil {
//Login failed, return response.
Expand Down Expand Up @@ -85,8 +97,7 @@ func (a *epinioAuth) GetUsername(userid string) (string, error) {
func (a *epinioAuth) GetUser(userGUID string) (*interfaces.ConnectedUser, error) {
log.Debug("GetUser")

var scopes []string
scopes = make([]string, 0) // User has no stratos scopes such as "stratos.admin", "password.write", "scim.write"
scopes := make([]string, 0) // User has no stratos scopes such as "stratos.admin", "password.write", "scim.write"

connectedUser := &interfaces.ConnectedUser{
GUID: userGUID,
Expand All @@ -102,21 +113,25 @@ func (a *epinioAuth) BeforeVerifySession(c echo.Context) {}

func (a *epinioAuth) VerifySession(c echo.Context, sessionUser string, sessionExpireTime int64) error {
// Never expires
// Only really used by `/v1/auth/verify`
// Only used by `/v1/auth/verify`
return nil
}

//epinioLogin verifies local user credentials against our DB
func (a *epinioAuth) epinioLogin(c echo.Context) (string, string, error) {
log.Debug("doLocalLogin")
//epinioLocalLogin verifies local user credentials
func (a *epinioAuth) epinioLocalLogin(c echo.Context) (string, string, error) {
log.Debug("epinioLocalLogin")

username, password, err := a.getRancherUsernameAndPassword(c)
if err != nil {
return "", "", err
msg := "unable to determine Username and/or password: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

if err := a.verifyEpinioCreds(username, password); err != nil {
return "", "", err
if err := a.verifyLocalLoginCreds(username, password); err != nil {
msg := "unable to verify Username and/or password: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

// User guid, user name, err
Expand All @@ -132,56 +147,46 @@ func (a *epinioAuth) getRancherUsernameAndPassword(c echo.Context) (string, stri

var params rancherproxy.LoginParams
if err = json.Unmarshal(body, &params); err != nil {
return "", "", err
return "", "", errors.New("failed to parse body with username/password")
}

username := params.Username
password := params.Password

if len(username) == 0 || len(password) == 0 {
return "", username, errors.New("Username and/or password required")
return "", username, errors.New("username and/or password required")
}

authString := fmt.Sprintf("%s:%s", username, password)
base64EncodedAuthString := base64.StdEncoding.EncodeToString([]byte(authString))

// Set these so they're available in the epinio plugin login
c.Set("rancher_username", username)
c.Set("rancher_password", password)
tr := &interfaces.TokenRecord{
AuthType: interfaces.AuthTypeHttpBasic,
AuthToken: base64EncodedAuthString,
RefreshToken: username,
}
c.Set("token", tr)

return username, password, nil
}

func (a *epinioAuth) verifyEpinioCreds(username, password string) error {
func (a *epinioAuth) verifyLocalLoginCreds(username, password string) error {
log.Debug("verifyEpinioCreds")

// Find the epinio endpoint
endpoints, err := a.p.ListEndpoints()
if err != nil {
msg := "Failed to fetch list of endpoints: %+v"
log.Errorf(msg, err)
return fmt.Errorf(msg, err)
}
epinioEndpoint, err := epinio_utils.FindEpinioEndpoint(a.p)

var epinioEndpoint *interfaces.CNSIRecord
for _, e := range endpoints {
if e.CNSIType == eInterfaces.EndpointType {
epinioEndpoint = e
break
}
}

if epinioEndpoint == nil {
msg := "Failed to find an epinio endpoint"
log.Error(msg)
return fmt.Errorf(msg)
if err != nil {
return fmt.Errorf("failed to find an epinio endpoint: %v", err)
}

// Make a request to the epinio endpoint that requires auth
credsUrl := fmt.Sprintf("%s/api/v1/info", epinioEndpoint.APIEndpoint.String())

req, err := http.NewRequest("GET", credsUrl, nil)
if err != nil {
msg := "Failed to create request to verify epinio creds: %v"
log.Errorf(msg, err)
return fmt.Errorf(msg, err)
return fmt.Errorf("failed to create request to verify epinio creds: %v", err)
}

req.SetBasicAuth(username, password)
Expand All @@ -199,6 +204,82 @@ func (a *epinioAuth) verifyEpinioCreds(username, password string) error {

}

// ------------------
//epinioOIDCLogin verifies DEX credentials
func (a *epinioAuth) epinioOIDCLogin(c echo.Context) (string, string, error) {
log.Debug("epinioOIDCLogin")

defer c.Request().Body.Close()
body, err := ioutil.ReadAll(c.Request().Body)
if err != nil {
msg := "unable to read body: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

var params rancherproxy.LoginOIDCParams
if err = json.Unmarshal(body, &params); err != nil {
msg := "unable to parse body: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

if len(params.Code) == 0 {
return "", "", errors.New("auth code required")
}

oidcProvider, err := a.p.GetDex()

if err != nil {
msg := "unable to create dex client: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

token, err := oidcProvider.ExchangeWithPKCE(c.Request().Context(), params.Code, params.CodeVerifier)
if err != nil {
msg := "failed to get token from code: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

tr := &interfaces.TokenRecord{
AuthType: interfaces.AuthTypeDex,
AuthToken: token.AccessToken,
RefreshToken: token.RefreshToken,
TokenExpiry: token.Expiry.Unix(),
Metadata: params.CodeVerifier, // This will be used for refreshing the token
}

idToken, err := oidcProvider.Verify(c.Request().Context(), token.AccessToken)
if err != nil {
msg := "failed to verify fetched token: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

var claims struct {
Email string `json:"email"`
Groups []string `json:"groups"`
Profile interface{} `json:"profile"`
}
log.Warnf("epinioOIDCLogin: token: %+v", idToken)

if err := idToken.Claims(&claims); err != nil {
msg := "token in unexpected format: %+v"
log.Errorf(msg, err)
return "", "", errors.New(msg)
}

log.Warnf("epinioOIDCLogin: claims: %+v", claims)

c.Set("token", tr)

return claims.Email, claims.Email, nil

}

// ------------------
//generateLoginSuccessResponse
func (e *epinioAuth) generateLoginSuccessResponse(c echo.Context, userGUID, username string) error {
log.Debug("generateLoginSuccessResponse")
Expand Down
3 changes: 2 additions & 1 deletion src/jetstream/cnsi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestRegisterCFCluster(t *testing.T) {
req := setupMockReq("POST", "", map[string]string{
"cnsi_name": "Some fancy CF Cluster",
"api_endpoint": mockV2Info.URL,
"auth_endpoint": mockV2Info.URL,
"skip_ssl_validation": "true",
"cnsi_client_id": mockClientId,
"cnsi_client_secret": mockClientSecret,
Expand All @@ -33,7 +34,7 @@ func TestRegisterCFCluster(t *testing.T) {
defer db.Close()

mock.ExpectExec(insertIntoCNSIs).
WithArgs(sqlmock.AnyArg(), "Some fancy CF Cluster", "epinio", mockV2Info.URL, "", "", "", true, mockClientId, sqlmock.AnyArg(), false, "", "").
WithArgs(sqlmock.AnyArg(), "Some fancy CF Cluster", "epinio", mockV2Info.URL, "abc", "", "abc", true, mockClientId, sqlmock.AnyArg(), false, "", "abc").
WillReturnResult(sqlmock.NewResult(1, 1))

if err := pp.RegisterEndpoint(ctx, getCFPlugin(pp, "epinio").Info); err != nil {
Expand Down
33 changes: 33 additions & 0 deletions src/jetstream/dex/code_verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dex

import (
"crypto/sha256"
"encoding/base64"

"github.com/dchest/uniuri"
)

// CodeVerifier is an helper struct used to create a code_challenge for the PKCE
// Ref: https://www.oauth.com/oauth2-servers/pkce/
type CodeVerifier struct {
Value string
}

// NewCodeVerifier returns a cryptographic secure random CodeVerifier of a fixed length (32)
func NewCodeVerifier() *CodeVerifier {
return NewCodeVerifierWithLen(32)
}

// NewCodeVerifier returns a cryptographic secure random CodeVerifier of the specified length
func NewCodeVerifierWithLen(len int) *CodeVerifier {
return &CodeVerifier{Value: uniuri.NewLen(len)}
}

// ChallengeS256 returns an encoded SHA256 code_challenge of the code_verifier
func (c *CodeVerifier) ChallengeS256() string {
h := sha256.New()
h.Write([]byte(c.Value))
hash := h.Sum(nil)

return base64.RawURLEncoding.EncodeToString(hash)
}
Loading