Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add login endpoints and UI scripts to use them #15

Merged
merged 2 commits into from
Sep 5, 2024
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
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 @@ -80,6 +80,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