Skip to content

Commit

Permalink
Merge pull request #15 from mrtamm/dev-ui-login-flow
Browse files Browse the repository at this point in the history
Add login endpoints and UI scripts to use them
  • Loading branch information
xhejtman authored Sep 5, 2024
2 parents cccb4b6 + 1ef9c8d commit 218b6e4
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 258 deletions.
2 changes: 1 addition & 1 deletion cmd/util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func MergeConfigFileWithFlags(file string, flagConf config.Config) (config.Confi
fmt.Println("Configuration problem: RPCClient User and Password " +
"are undefined while Server.BasicAuth is enforced.")
os.Exit(1)
} else if conf.Server.OidcAuth.ServiceConfigUrl != "" {
} else if conf.Server.OidcAuth.ServiceConfigURL != "" {
// Generating random user/password credentials for RPC:
conf.RPCClient.User = randomCredential()
conf.RPCClient.Password = randomCredential()
Expand Down
3 changes: 2 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ type BasicCredential struct {
}

type OidcAuth struct {
ServiceConfigUrl string
ServiceConfigURL string
ClientId string
ClientSecret string
RequireScope string
RequireAudience string
RedirectURL string
}

// RPCClient describes configuration for gRPC clients
Expand Down
4 changes: 3 additions & 1 deletion config/default-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Server:
# Server won't launch when configuration URL cannot be loaded.
# OidcAuth:
# # URL of the OIDC service configuration:
# ServiceConfigUrl: ""
# ServiceConfigURL: ""
# # Client ID and secret are sent with the token introspection request
# # (Basic authentication):
# ClientId:
Expand All @@ -50,6 +50,8 @@ Server:
# RequireScope:
# # Optional: if specified, this audience value must be in the token:
# RequireAudience:
# # The URL where OIDC should redirect after login (keep the path '/login')
# RedirectURL: "http://localhost:8000/login"

# Include a "Cache-Control: no-store" HTTP header in Get/List responses
# to prevent caching by intermediary services.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ require (
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gammazero/deque v0.2.0 // indirect
github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect
github.com/go-ini/ini v1.52.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ github.com/gizak/termui v2.3.0+incompatible h1:S8wJoNumYfc/rR5UezUM4HsPEo3RJh0LK
github.com/gizak/termui v2.3.0+incompatible/go.mod h1:PkJoWUt/zacQKysNfQtcw1RW+eK2SxkieVBtl+4ovLA=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE=
github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-ini/ini v1.52.0 h1:3UeUAveYUTCYV/G0jNDiIrrtIeAl1oAjshYyU2PaAlQ=
github.com/go-ini/ini v1.52.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
Expand Down
133 changes: 90 additions & 43 deletions server/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server

import (
"encoding/base64"
"net/http"
"strings"

"github.com/ohsu-comp-bio/funnel/config"
Expand All @@ -12,68 +13,114 @@ import (
"google.golang.org/grpc/status"
)

type Authentication struct {
basic map[string]bool
oidc *OidcConfig
}

var (
errMissingMetadata = status.Errorf(codes.InvalidArgument, "Missing metadata in the context")
errTokenRequired = status.Errorf(codes.Unauthenticated, "Basic/Bearer authorization token missing")
errInvalidBasicToken = status.Errorf(codes.Unauthenticated, "Invalid Basic authorization token")
errInvalidBearerToken = status.Errorf(codes.Unauthenticated, "Invalid Bearer authorization token")
)

// Return a new interceptor function that authorizes RPCs.
func newAuthInterceptor(creds []config.BasicCredential, oidc config.OidcAuth) grpc.UnaryServerInterceptor {
basicCreds := initBasicCredsMap(creds)
oidcConfig := initOidcConfig(oidc)
requireAuth := len(basicCreds) > 0 || oidcConfig != nil

// Return a function that is the interceptor.
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {

if !requireAuth {
return handler(ctx, req)
}
func NewAuthentication(creds []config.BasicCredential, oidc config.OidcAuth) *Authentication {
basicCreds := make(map[string]bool)
for _, cred := range creds {
credBytes := []byte(cred.User + ":" + cred.Password)
fullValue := "Basic " + base64.StdEncoding.EncodeToString(credBytes)
basicCreds[fullValue] = true
}

md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errMissingMetadata
}
return &Authentication{
basic: basicCreds,
oidc: initOidcConfig(oidc),
}
}

values := md["authorization"]
if len(values) < 1 {
return nil, errTokenRequired
}
// Return a new gRPC interceptor function that authorizes RPCs.
func (a *Authentication) Interceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {

authorized := false
auth_err := errTokenRequired
// Case when authentication is not required:
if len(a.basic) == 0 && a.oidc == nil {
return handler(ctx, req)
}

md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errMissingMetadata
}

if strings.HasPrefix(values[0], "Basic ") {
auth_err = errInvalidBasicToken
authorized = basicCreds[values[0]]
} else if oidcConfig != nil && strings.HasPrefix(values[0], "Bearer ") {
auth_err = errInvalidBearerToken
values := md["authorization"]
if len(values) == 0 {
return nil, errTokenRequired
}

authorized := false
authErr := errTokenRequired

if strings.HasPrefix(values[0], "Basic ") {
authErr = errInvalidBasicToken
headerValue := values[0]
authorized = a.basic[headerValue]
} else if a.oidc != nil {
if strings.HasPrefix(values[0], "Bearer ") {
authErr = errInvalidBearerToken
jwtString := strings.TrimPrefix(values[0], "Bearer ")
jwt := oidcConfig.ParseJwt(jwtString)
jwt := a.oidc.ParseJwt(jwtString)
authorized = jwt != nil
}
}

if !authorized {
return nil, auth_err
}
if !authorized {
return nil, authErr
}

return handler(ctx, req)
return handler(ctx, req)
}

// HTTP request handler for the /login endpoint. Initiates user authentication
// flow based on the configuration (OIDC, Basic, none).
func (a *Authentication) LoginHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(w, "Only GET method is supported.", http.StatusMethodNotAllowed)
} else if a.oidc != nil {
a.oidc.HandleAuthCode(w, req)
} else if len(a.basic) > 0 {
a.handleBasicAuth(w, req)
} else {
http.Redirect(w, req, "/", http.StatusSeeOther)
}
}

func initBasicCredsMap(creds []config.BasicCredential) map[string]bool {
basicCreds := make(map[string]bool)
for _, cred := range creds {
credBytes := []byte(cred.User + ":" + cred.Password)
fullValue := "Basic " + base64.StdEncoding.EncodeToString(credBytes)
basicCreds[fullValue] = true
// HTTP request handler for the /login/token endpoint. In case of OIDC enabled,
// prints the JWT from the sent cookie. In all other cases, an empty HTTP 200
// response.
func (a *Authentication) EchoTokenHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(w, "Only GET method is supported.", http.StatusMethodNotAllowed)
} else if a.oidc != nil {
a.oidc.EchoTokenHandler(w, req)
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusOK)
}
}

return basicCreds
func (a *Authentication) handleBasicAuth(w http.ResponseWriter, req *http.Request) {
// Check if provided value in the header is valid:
if a.basic[req.Header.Get("Authorization")] {
http.Redirect(w, req, "/", http.StatusSeeOther)
} else {
w.Header().Set("WWW-Authenticate", "Basic realm=Funnel")
msg := "User authentication is required (Basic authentication with " +
"username and password)"
http.Error(w, msg, http.StatusUnauthorized)
}
}
Loading

0 comments on commit 218b6e4

Please sign in to comment.