mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-24 02:19:05 +01:00
refactor auth module
Restructure module Separate JWT auth variants Cleanup code Fixes #189
This commit is contained in:
parent
bc6e6250e1
commit
b8273a9b02
@ -1,13 +1,15 @@
|
|||||||
# Overview
|
# Overview
|
||||||
|
|
||||||
The implementation of authentication is not easy to understand by just looking
|
The authentication is implemented in `internal/auth/`. In `auth.go`
|
||||||
at the code. The authentication is implemented in `internal/auth/`. In `auth.go`
|
|
||||||
an interface is defined that any authentication provider must fulfill. It also
|
an interface is defined that any authentication provider must fulfill. It also
|
||||||
acts as a dispatcher to delegate the calls to the available authentication
|
acts as a dispatcher to delegate the calls to the available authentication
|
||||||
providers.
|
providers.
|
||||||
|
|
||||||
The most important routine are:
|
Two authentication types are available:
|
||||||
* `CanLogin()` Check if the authentication method is supported for login attempt
|
* JWT authentication for the REST API that does not create a session cookie
|
||||||
|
* Session based authentication using a session cookie
|
||||||
|
|
||||||
|
The most important routines in auth are:
|
||||||
* `Login()` Handle POST request to login user and start a new session
|
* `Login()` Handle POST request to login user and start a new session
|
||||||
* `Auth()` Authenticate user and put User Object in context of the request
|
* `Auth()` Authenticate user and put User Object in context of the request
|
||||||
|
|
||||||
@ -30,10 +32,9 @@ secured.Use(func(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
For non API routes a JWT token can be used to initiate an authenticated user
|
A JWT token can be used to initiate an authenticated user
|
||||||
session. This can either happen by calling the login route with a token
|
session. This can either happen by calling the login route with a token
|
||||||
provided in a header or query URL or via the `Auth()` method on first access
|
provided in a header or via a special cookie containing the JWT token.
|
||||||
to a secured URL via a special cookie containing the JWT token.
|
|
||||||
For API routes the access is authenticated on every request using the JWT token
|
For API routes the access is authenticated on every request using the JWT token
|
||||||
and no session is initiated.
|
and no session is initiated.
|
||||||
|
|
||||||
@ -43,12 +44,13 @@ The Login function (located in `auth.go`):
|
|||||||
* Extracts the user name and gets the user from the user database table. In case the
|
* Extracts the user name and gets the user from the user database table. In case the
|
||||||
user is not found the user object is set to nil.
|
user is not found the user object is set to nil.
|
||||||
* Iterates over all authenticators and:
|
* Iterates over all authenticators and:
|
||||||
- Calls the `CanLogin` function which checks if the authentication method is
|
- Calls its `CanLogin` function which checks if the authentication method is
|
||||||
supported for this user and the user object is valid.
|
supported for this user.
|
||||||
- Calls the `Login` function to authenticate the user. On success a valid user
|
- Calls its `Login` function to authenticate the user. On success a valid user
|
||||||
object is returned.
|
object is returned.
|
||||||
- Creates a new session object, stores the user attributes in the session and
|
- Creates a new session object, stores the user attributes in the session and
|
||||||
saves the session.
|
saves the session.
|
||||||
|
- If the user does not yet exist in the database try to add the user
|
||||||
- Starts the `onSuccess` http handler
|
- Starts the `onSuccess` http handler
|
||||||
|
|
||||||
## Local authenticator
|
## Local authenticator
|
||||||
@ -82,17 +84,13 @@ if err := l.Bind(userDn, r.FormValue("password")); err != nil {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## JWT authenticator
|
## JWT Session authenticator
|
||||||
|
|
||||||
Login via JWT token will create a session without password.
|
Login via JWT token will create a session without password.
|
||||||
For login the `X-Auth-Token` header is not supported.
|
For login the `X-Auth-Token` header is not supported. This authenticator is
|
||||||
This authenticator is applied if either user is not nil and auth source is
|
applied if the Authorization header is present:
|
||||||
`AuthViaToken` or the Authorization header is present or the URL query key
|
|
||||||
login-token is present:
|
|
||||||
```
|
```
|
||||||
return (user != nil && user.AuthSource == AuthViaToken) ||
|
return r.Header.Get("Authorization") != ""
|
||||||
r.Header.Get("Authorization") != "" ||
|
|
||||||
r.URL.Query().Get("login-token") != ""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The Login function:
|
The Login function:
|
||||||
@ -108,6 +106,25 @@ The Login function:
|
|||||||
- In case user is not yet present add user to user database table with `AuthViaToken` AuthSource.
|
- In case user is not yet present add user to user database table with `AuthViaToken` AuthSource.
|
||||||
* Return valid user object
|
* Return valid user object
|
||||||
|
|
||||||
|
## JWT Cookie Session authenticator
|
||||||
|
|
||||||
|
Login via JWT cookie token will create a session without password.
|
||||||
|
It is first checked if the required configuration keys are set:
|
||||||
|
* `publicKeyCrossLogin`
|
||||||
|
* `TrustedExternalIssuer`
|
||||||
|
* `CookieName`
|
||||||
|
|
||||||
|
This authenticator is applied if the configured cookie is present:
|
||||||
|
```
|
||||||
|
jwtCookie, err := r.Cookie(cookieName)
|
||||||
|
|
||||||
|
if err == nil && jwtCookie.Value != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The Login function:
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
|
|
||||||
The Auth function (located in `auth.go`):
|
The Auth function (located in `auth.go`):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
@ -7,12 +7,11 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||||
@ -28,172 +27,25 @@ const (
|
|||||||
AuthViaToken
|
AuthViaToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AuthType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthToken AuthType = iota
|
||||||
|
AuthSession
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
AuthSource AuthSource `json:"via"`
|
AuthType AuthType `json:"authType"`
|
||||||
|
AuthSource AuthSource `json:"authSource"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Projects []string `json:"projects"`
|
Projects []string `json:"projects"`
|
||||||
Expiration time.Time
|
Expiration time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Role int
|
|
||||||
|
|
||||||
const (
|
|
||||||
RoleAnonymous Role = iota
|
|
||||||
RoleApi
|
|
||||||
RoleUser
|
|
||||||
RoleManager
|
|
||||||
RoleSupport
|
|
||||||
RoleAdmin
|
|
||||||
RoleError
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetRoleString(roleInt Role) string {
|
|
||||||
return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRoleEnum(roleStr string) Role {
|
|
||||||
switch strings.ToLower(roleStr) {
|
|
||||||
case "admin":
|
|
||||||
return RoleAdmin
|
|
||||||
case "support":
|
|
||||||
return RoleSupport
|
|
||||||
case "manager":
|
|
||||||
return RoleManager
|
|
||||||
case "user":
|
|
||||||
return RoleUser
|
|
||||||
case "api":
|
|
||||||
return RoleApi
|
|
||||||
case "anonymous":
|
|
||||||
return RoleAnonymous
|
|
||||||
default:
|
|
||||||
return RoleError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidRole(role string) bool {
|
|
||||||
return getRoleEnum(role) != RoleError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
|
|
||||||
if isValidRole(role) {
|
|
||||||
for _, r := range u.Roles {
|
|
||||||
if r == role {
|
|
||||||
return true, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, true
|
|
||||||
}
|
|
||||||
return false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) HasRole(role Role) bool {
|
|
||||||
for _, r := range u.Roles {
|
|
||||||
if r == GetRoleString(role) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role-Arrays are short: performance not impacted by nested loop
|
|
||||||
func (u *User) HasAnyRole(queryroles []Role) bool {
|
|
||||||
for _, ur := range u.Roles {
|
|
||||||
for _, qr := range queryroles {
|
|
||||||
if ur == GetRoleString(qr) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role-Arrays are short: performance not impacted by nested loop
|
|
||||||
func (u *User) HasAllRoles(queryroles []Role) bool {
|
|
||||||
target := len(queryroles)
|
|
||||||
matches := 0
|
|
||||||
for _, ur := range u.Roles {
|
|
||||||
for _, qr := range queryroles {
|
|
||||||
if ur == GetRoleString(qr) {
|
|
||||||
matches += 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches == target {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role-Arrays are short: performance not impacted by nested loop
|
|
||||||
func (u *User) HasNotRoles(queryroles []Role) bool {
|
|
||||||
matches := 0
|
|
||||||
for _, ur := range u.Roles {
|
|
||||||
for _, qr := range queryroles {
|
|
||||||
if ur == GetRoleString(qr) {
|
|
||||||
matches += 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches == 0 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by API endpoint '/roles/' from frontend: Only required for admin config -> Check Admin Role
|
|
||||||
func GetValidRoles(user *User) ([]string, error) {
|
|
||||||
var vals []string
|
|
||||||
if user.HasRole(RoleAdmin) {
|
|
||||||
for i := RoleApi; i < RoleError; i++ {
|
|
||||||
vals = append(vals, GetRoleString(i))
|
|
||||||
}
|
|
||||||
return vals, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by routerConfig web.page setup in backend: Only requires known user
|
|
||||||
func GetValidRolesMap(user *User) (map[string]Role, error) {
|
|
||||||
named := make(map[string]Role)
|
|
||||||
if user.HasNotRoles([]Role{RoleAnonymous}) {
|
|
||||||
for i := RoleApi; i < RoleError; i++ {
|
|
||||||
named[GetRoleString(i)] = i
|
|
||||||
}
|
|
||||||
return named, nil
|
|
||||||
}
|
|
||||||
return named, fmt.Errorf("only known users are allowed to fetch a list of roles")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find highest role
|
|
||||||
func (u *User) GetAuthLevel() Role {
|
|
||||||
if u.HasRole(RoleAdmin) {
|
|
||||||
return RoleAdmin
|
|
||||||
} else if u.HasRole(RoleSupport) {
|
|
||||||
return RoleSupport
|
|
||||||
} else if u.HasRole(RoleManager) {
|
|
||||||
return RoleManager
|
|
||||||
} else if u.HasRole(RoleUser) {
|
|
||||||
return RoleUser
|
|
||||||
} else if u.HasRole(RoleApi) {
|
|
||||||
return RoleApi
|
|
||||||
} else if u.HasRole(RoleAnonymous) {
|
|
||||||
return RoleAnonymous
|
|
||||||
} else {
|
|
||||||
return RoleError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) HasProject(project string) bool {
|
func (u *User) HasProject(project string) bool {
|
||||||
for _, p := range u.Projects {
|
for _, p := range u.Projects {
|
||||||
if p == project {
|
if p == project {
|
||||||
@ -216,7 +68,6 @@ type Authenticator interface {
|
|||||||
Init(auth *Authentication, config interface{}) error
|
Init(auth *Authentication, config interface{}) error
|
||||||
CanLogin(user *User, rw http.ResponseWriter, r *http.Request) bool
|
CanLogin(user *User, rw http.ResponseWriter, r *http.Request) bool
|
||||||
Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error)
|
Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error)
|
||||||
Auth(rw http.ResponseWriter, r *http.Request) (*User, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextKey string
|
type ContextKey string
|
||||||
@ -234,6 +85,47 @@ type Authentication struct {
|
|||||||
LocalAuth *LocalAuthenticator
|
LocalAuth *LocalAuthenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *Authentication) AuthViaSession(
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
r *http.Request) (*User, error) {
|
||||||
|
session, err := auth.sessionStore.Get(r, "session")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error while getting session store")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.IsNew {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var username string
|
||||||
|
var projects, roles []string
|
||||||
|
|
||||||
|
if val, ok := session.Values["username"]; ok {
|
||||||
|
username, _ = val.(string)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("No key username in session")
|
||||||
|
}
|
||||||
|
if val, ok := session.Values["projects"]; ok {
|
||||||
|
projects, _ = val.([]string)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("No key projects in session")
|
||||||
|
}
|
||||||
|
if val, ok := session.Values["projects"]; ok {
|
||||||
|
roles, _ = val.([]string)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("No key roles in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &User{
|
||||||
|
Username: username,
|
||||||
|
Projects: projects,
|
||||||
|
Roles: roles,
|
||||||
|
AuthType: AuthSession,
|
||||||
|
AuthSource: -1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func Init(db *sqlx.DB,
|
func Init(db *sqlx.DB,
|
||||||
configs map[string]interface{}) (*Authentication, error) {
|
configs map[string]interface{}) (*Authentication, error) {
|
||||||
auth := &Authentication{}
|
auth := &Authentication{}
|
||||||
@ -257,19 +149,11 @@ func Init(db *sqlx.DB,
|
|||||||
auth.sessionStore = sessions.NewCookieStore(bytes)
|
auth.sessionStore = sessions.NewCookieStore(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.LocalAuth = &LocalAuthenticator{}
|
|
||||||
if err := auth.LocalAuth.Init(auth, nil); err != nil {
|
|
||||||
log.Error("Error while initializing authentication -> localAuth init failed")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
auth.authenticators = append(auth.authenticators, auth.LocalAuth)
|
|
||||||
|
|
||||||
auth.JwtAuth = &JWTAuthenticator{}
|
auth.JwtAuth = &JWTAuthenticator{}
|
||||||
if err := auth.JwtAuth.Init(auth, configs["jwt"]); err != nil {
|
if err := auth.JwtAuth.Init(auth, configs["jwt"]); err != nil {
|
||||||
log.Error("Error while initializing authentication -> jwtAuth init failed")
|
log.Error("Error while initializing authentication -> jwtAuth init failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
auth.authenticators = append(auth.authenticators, auth.JwtAuth)
|
|
||||||
|
|
||||||
if config, ok := configs["ldap"]; ok {
|
if config, ok := configs["ldap"]; ok {
|
||||||
auth.LdapAuth = &LdapAuthenticator{}
|
auth.LdapAuth = &LdapAuthenticator{}
|
||||||
@ -280,36 +164,30 @@ func Init(db *sqlx.DB,
|
|||||||
auth.authenticators = append(auth.authenticators, auth.LdapAuth)
|
auth.authenticators = append(auth.authenticators, auth.LdapAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jwtSessionAuth := &JWTSessionAuthenticator{}
|
||||||
|
if err := jwtSessionAuth.Init(auth, configs["jwt"]); err != nil {
|
||||||
|
log.Warn("Error while initializing authentication -> jwtSessionAuth init failed")
|
||||||
|
} else {
|
||||||
|
auth.authenticators = append(auth.authenticators, jwtSessionAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
|
||||||
|
if err := jwtSessionAuth.Init(auth, configs["jwt"]); err != nil {
|
||||||
|
log.Warn("Error while initializing authentication -> jwtCookieSessionAuth init failed")
|
||||||
|
} else {
|
||||||
|
auth.authenticators = append(auth.authenticators, jwtCookieSessionAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.LocalAuth = &LocalAuthenticator{}
|
||||||
|
if err := auth.LocalAuth.Init(auth, nil); err != nil {
|
||||||
|
log.Error("Error while initializing authentication -> localAuth init failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
auth.authenticators = append(auth.authenticators, auth.LocalAuth)
|
||||||
|
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Authentication) AuthViaSession(
|
|
||||||
rw http.ResponseWriter,
|
|
||||||
r *http.Request) (*User, error) {
|
|
||||||
|
|
||||||
session, err := auth.sessionStore.Get(r, "session")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error while getting session store")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.IsNew {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Check if keys are present in session?
|
|
||||||
username, _ := session.Values["username"].(string)
|
|
||||||
projects, _ := session.Values["projects"].([]string)
|
|
||||||
roles, _ := session.Values["roles"].([]string)
|
|
||||||
return &User{
|
|
||||||
Username: username,
|
|
||||||
Projects: projects,
|
|
||||||
Roles: roles,
|
|
||||||
AuthSource: -1,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle a POST request that should log the user in, starting a new session.
|
|
||||||
func (auth *Authentication) Login(
|
func (auth *Authentication) Login(
|
||||||
onsuccess http.Handler,
|
onsuccess http.Handler,
|
||||||
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
|
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
|
||||||
@ -317,18 +195,21 @@ func (auth *Authentication) Login(
|
|||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
err := errors.New("no authenticator applied")
|
err := errors.New("no authenticator applied")
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
user := (*User)(nil)
|
dbUser := (*User)(nil)
|
||||||
|
|
||||||
if username != "" {
|
if username != "" {
|
||||||
user, _ = auth.GetUser(username)
|
dbUser, err = auth.GetUser(username)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
log.Errorf("Error while loading user '%v'", username)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, authenticator := range auth.authenticators {
|
for _, authenticator := range auth.authenticators {
|
||||||
if !authenticator.CanLogin(user, rw, r) {
|
if !authenticator.CanLogin(dbUser, rw, r) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err = authenticator.Login(user, rw, r)
|
user, err := authenticator.Login(dbUser, rw, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("user login failed: %s", err.Error())
|
log.Warnf("user login failed: %s", err.Error())
|
||||||
onfailure(rw, r, err)
|
onfailure(rw, r, err)
|
||||||
@ -354,6 +235,14 @@ func (auth *Authentication) Login(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbUser == nil {
|
||||||
|
if err := auth.AddUser(user); err != nil {
|
||||||
|
// TODO Add AuthSource
|
||||||
|
log.Errorf("Error while adding user '%v' to auth from XX",
|
||||||
|
user.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
|
log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
|
||||||
ctx := context.WithValue(r.Context(), ContextUserKey, user)
|
ctx := context.WithValue(r.Context(), ContextUserKey, user)
|
||||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||||
@ -365,39 +254,34 @@ func (auth *Authentication) Login(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate the user and put a User object in the
|
|
||||||
// context of the request. If authentication fails,
|
|
||||||
// do not continue but send client to the login screen.
|
|
||||||
func (auth *Authentication) Auth(
|
func (auth *Authentication) Auth(
|
||||||
onsuccess http.Handler,
|
onsuccess http.Handler,
|
||||||
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error)) http.Handler {
|
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error)) http.Handler {
|
||||||
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
for _, authenticator := range auth.authenticators {
|
|
||||||
user, err := authenticator.Auth(rw, r)
|
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
||||||
|
if user == nil {
|
||||||
|
user, err = auth.AuthViaSession(rw, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Infof("authentication failed: %s", err.Error())
|
log.Infof("authentication failed: %s", err.Error())
|
||||||
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user == nil {
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
ctx := context.WithValue(r.Context(), ContextUserKey, user)
|
ctx := context.WithValue(r.Context(), ContextUserKey, user)
|
||||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("authentication failed: %s", "no authenticator applied")
|
log.Debug("authentication failed: no authenticator applied")
|
||||||
// http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
onfailure(rw, r, errors.New("unauthorized (please login first)"))
|
||||||
onfailure(rw, r, errors.New("unauthorized (login first or use a token)"))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clears the session cookie
|
|
||||||
func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
|
func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
|
||||||
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
session, err := auth.sessionStore.Get(r, "session")
|
session, err := auth.sessionStore.Get(r, "session")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,10 +6,8 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"database/sql"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -23,17 +21,11 @@ import (
|
|||||||
type JWTAuthenticator struct {
|
type JWTAuthenticator struct {
|
||||||
auth *Authentication
|
auth *Authentication
|
||||||
|
|
||||||
publicKey ed25519.PublicKey
|
publicKey ed25519.PublicKey
|
||||||
privateKey ed25519.PrivateKey
|
privateKey ed25519.PrivateKey
|
||||||
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
|
config *schema.JWTAuthConfig
|
||||||
|
|
||||||
loginTokenKey []byte // HS256 key
|
|
||||||
|
|
||||||
config *schema.JWTAuthConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Authenticator = (*JWTAuthenticator)(nil)
|
|
||||||
|
|
||||||
func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
|
func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
|
||||||
|
|
||||||
ja.auth = auth
|
ja.auth = auth
|
||||||
@ -57,128 +49,10 @@ func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
|
|||||||
ja.privateKey = ed25519.PrivateKey(bytes)
|
ja.privateKey = ed25519.PrivateKey(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pubKey = os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
|
|
||||||
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode cross login JWT HS512 key")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ja.loginTokenKey = bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for external public keys
|
|
||||||
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
|
|
||||||
if keyFound && pubKeyCrossLogin != "" {
|
|
||||||
bytes, err := base64.StdEncoding.DecodeString(pubKeyCrossLogin)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode cross login JWT public key")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ja.publicKeyCrossLogin = ed25519.PublicKey(bytes)
|
|
||||||
|
|
||||||
// Warn if other necessary settings are not configured
|
|
||||||
if ja.config != nil {
|
|
||||||
if ja.config.CookieName == "" {
|
|
||||||
log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
|
|
||||||
}
|
|
||||||
if !ja.config.ForceJWTValidationViaDatabase {
|
|
||||||
log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
|
|
||||||
}
|
|
||||||
if ja.config.TrustedExternalIssuer == "" {
|
|
||||||
log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Warn("cookieName and trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ja.publicKeyCrossLogin = nil
|
|
||||||
log.Debug("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ja *JWTAuthenticator) CanLogin(
|
func (ja *JWTAuthenticator) AuthViaJWT(
|
||||||
user *User,
|
|
||||||
rw http.ResponseWriter,
|
|
||||||
r *http.Request) bool {
|
|
||||||
|
|
||||||
return (user != nil && user.AuthSource == AuthViaToken) ||
|
|
||||||
r.Header.Get("Authorization") != "" ||
|
|
||||||
r.URL.Query().Get("login-token") != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ja *JWTAuthenticator) Login(
|
|
||||||
user *User,
|
|
||||||
rw http.ResponseWriter,
|
|
||||||
r *http.Request) (*User, error) {
|
|
||||||
|
|
||||||
rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
|
||||||
if rawtoken == "" {
|
|
||||||
rawtoken = r.URL.Query().Get("login-token")
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
|
||||||
if t.Method == jwt.SigningMethodEdDSA {
|
|
||||||
return ja.publicKey, nil
|
|
||||||
}
|
|
||||||
if t.Method == jwt.SigningMethodHS256 || t.Method == jwt.SigningMethodHS512 {
|
|
||||||
return ja.loginTokenKey, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("AUTH/JWT > unkown signing method for login token: %s (known: HS256, HS512, EdDSA)", t.Method.Alg())
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error while parsing jwt token")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = token.Claims.Valid(); err != nil {
|
|
||||||
log.Warn("jwt token claims are not valid")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
|
||||||
sub, _ := claims["sub"].(string)
|
|
||||||
exp, _ := claims["exp"].(float64)
|
|
||||||
var roles []string
|
|
||||||
if rawroles, ok := claims["roles"].([]interface{}); ok {
|
|
||||||
for _, rr := range rawroles {
|
|
||||||
if r, ok := rr.(string); ok {
|
|
||||||
if isValidRole(r) {
|
|
||||||
roles = append(roles, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rawrole, ok := claims["roles"].(string); ok {
|
|
||||||
if isValidRole(rawrole) {
|
|
||||||
roles = append(roles, rawrole)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
user, err = ja.auth.GetUser(sub)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
log.Errorf("Error while loading user '%v'", sub)
|
|
||||||
return nil, err
|
|
||||||
} else if user == nil {
|
|
||||||
user = &User{
|
|
||||||
Username: sub,
|
|
||||||
Roles: roles,
|
|
||||||
AuthSource: AuthViaToken,
|
|
||||||
}
|
|
||||||
if err := ja.auth.AddUser(user); err != nil {
|
|
||||||
log.Errorf("Error while adding user '%v' to auth from token", user.Username)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Expiration = time.Unix(int64(exp), 0)
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ja *JWTAuthenticator) Auth(
|
|
||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) (*User, error) {
|
r *http.Request) (*User, error) {
|
||||||
|
|
||||||
@ -188,59 +62,17 @@ func (ja *JWTAuthenticator) Auth(
|
|||||||
rawtoken = strings.TrimPrefix(rawtoken, "Bearer ")
|
rawtoken = strings.TrimPrefix(rawtoken, "Bearer ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no auth header was found, check for a certain cookie containing a JWT
|
|
||||||
cookieName := ""
|
|
||||||
cookieFound := false
|
|
||||||
if ja.config != nil && ja.config.CookieName != "" {
|
|
||||||
cookieName = ja.config.CookieName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to read the JWT cookie
|
|
||||||
if rawtoken == "" && cookieName != "" {
|
|
||||||
jwtCookie, err := r.Cookie(cookieName)
|
|
||||||
|
|
||||||
if err == nil && jwtCookie.Value != "" {
|
|
||||||
rawtoken = jwtCookie.Value
|
|
||||||
cookieFound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because a user can also log in via a token, the
|
|
||||||
// session cookie must be checked here as well:
|
|
||||||
if rawtoken == "" {
|
|
||||||
return ja.auth.AuthViaSession(rw, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse JWT
|
|
||||||
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
||||||
if t.Method != jwt.SigningMethodEdDSA {
|
if t.Method != jwt.SigningMethodEdDSA {
|
||||||
return nil, errors.New("only Ed25519/EdDSA supported")
|
return nil, errors.New("only Ed25519/EdDSA supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is there more than one public key?
|
|
||||||
if ja.publicKeyCrossLogin != nil &&
|
|
||||||
ja.config != nil &&
|
|
||||||
ja.config.TrustedExternalIssuer != "" {
|
|
||||||
|
|
||||||
// Determine whether to use the external public key
|
|
||||||
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
|
|
||||||
if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer {
|
|
||||||
// The (unvalidated) issuer seems to be the expected one,
|
|
||||||
// use public cross login key from config
|
|
||||||
return ja.publicKeyCrossLogin, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No cross login key configured or issuer not expected
|
|
||||||
// Try own key
|
|
||||||
return ja.publicKey, nil
|
return ja.publicKey, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while parsing token")
|
log.Warn("Error while parsing JWT token")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check token validity
|
|
||||||
if err := token.Claims.Valid(); err != nil {
|
if err := token.Claims.Valid(); err != nil {
|
||||||
log.Warn("jwt token claims are not valid")
|
log.Warn("jwt token claims are not valid")
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -261,7 +93,6 @@ func (ja *JWTAuthenticator) Auth(
|
|||||||
log.Warn("Could not find user from JWT in internal database.")
|
log.Warn("Could not find user from JWT in internal database.")
|
||||||
return nil, errors.New("unknown user")
|
return nil, errors.New("unknown user")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take user roles from database instead of trusting the JWT
|
// Take user roles from database instead of trusting the JWT
|
||||||
roles = user.Roles
|
roles = user.Roles
|
||||||
} else {
|
} else {
|
||||||
@ -275,41 +106,10 @@ func (ja *JWTAuthenticator) Auth(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookieFound {
|
|
||||||
// Create a session so that we no longer need the JTW Cookie
|
|
||||||
session, err := ja.auth.sessionStore.New(r, "session")
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("session creation failed: %s", err.Error())
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ja.auth.SessionMaxAge != 0 {
|
|
||||||
session.Options.MaxAge = int(ja.auth.SessionMaxAge.Seconds())
|
|
||||||
}
|
|
||||||
session.Values["username"] = sub
|
|
||||||
session.Values["roles"] = roles
|
|
||||||
|
|
||||||
if err := ja.auth.sessionStore.Save(r, rw, session); err != nil {
|
|
||||||
log.Warnf("session save failed: %s", err.Error())
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// (Ask browser to) Delete JWT cookie
|
|
||||||
deletedCookie := &http.Cookie{
|
|
||||||
Name: cookieName,
|
|
||||||
Value: "",
|
|
||||||
Path: "/",
|
|
||||||
MaxAge: -1,
|
|
||||||
HttpOnly: true,
|
|
||||||
}
|
|
||||||
http.SetCookie(rw, deletedCookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &User{
|
return &User{
|
||||||
Username: sub,
|
Username: sub,
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
|
AuthType: AuthSession,
|
||||||
AuthSource: AuthViaToken,
|
AuthSource: AuthViaToken,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
224
internal/auth/jwtCookieSession.go
Normal file
224
internal/auth/jwtCookieSession.go
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
|
||||||
|
// All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||||
|
"github.com/ClusterCockpit/cc-backend/pkg/schema"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWTCookieSessionAuthenticator struct {
|
||||||
|
auth *Authentication
|
||||||
|
|
||||||
|
publicKey ed25519.PublicKey
|
||||||
|
privateKey ed25519.PrivateKey
|
||||||
|
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
|
||||||
|
|
||||||
|
loginTokenKey []byte // HS256 key
|
||||||
|
|
||||||
|
config *schema.JWTAuthConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil)
|
||||||
|
|
||||||
|
func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
|
||||||
|
|
||||||
|
ja.auth = auth
|
||||||
|
ja.config = conf.(*schema.JWTAuthConfig)
|
||||||
|
|
||||||
|
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
|
||||||
|
if pubKey == "" || privKey == "" {
|
||||||
|
log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
|
||||||
|
return errors.New("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
|
||||||
|
} else {
|
||||||
|
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not decode JWT public key")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ja.publicKey = ed25519.PublicKey(bytes)
|
||||||
|
bytes, err = base64.StdEncoding.DecodeString(privKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not decode JWT private key")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ja.privateKey = ed25519.PrivateKey(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubKey = os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
|
||||||
|
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not decode cross login JWT HS512 key")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ja.loginTokenKey = bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for external public keys
|
||||||
|
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
|
||||||
|
if keyFound && pubKeyCrossLogin != "" {
|
||||||
|
bytes, err := base64.StdEncoding.DecodeString(pubKeyCrossLogin)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not decode cross login JWT public key")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ja.publicKeyCrossLogin = ed25519.PublicKey(bytes)
|
||||||
|
} else {
|
||||||
|
ja.publicKeyCrossLogin = nil
|
||||||
|
log.Debug("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
|
||||||
|
return errors.New("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if other necessary settings are not configured
|
||||||
|
if ja.config != nil {
|
||||||
|
if ja.config.CookieName == "" {
|
||||||
|
log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
|
||||||
|
return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
|
||||||
|
}
|
||||||
|
if !ja.config.ForceJWTValidationViaDatabase {
|
||||||
|
log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
|
||||||
|
}
|
||||||
|
if ja.config.TrustedExternalIssuer == "" {
|
||||||
|
log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
|
||||||
|
return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warn("config for JWTs not configured (cross login via JWT cookie will fail)")
|
||||||
|
return errors.New("config for JWTs not configured (cross login via JWT cookie will fail)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ja *JWTCookieSessionAuthenticator) CanLogin(
|
||||||
|
user *User,
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
r *http.Request) bool {
|
||||||
|
|
||||||
|
if ja.publicKeyCrossLogin == nil ||
|
||||||
|
ja.config == nil ||
|
||||||
|
ja.config.TrustedExternalIssuer == "" {
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieName := ""
|
||||||
|
if ja.config != nil && ja.config.CookieName != "" {
|
||||||
|
cookieName = ja.config.CookieName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read the JWT cookie
|
||||||
|
if cookieName != "" {
|
||||||
|
jwtCookie, err := r.Cookie(cookieName)
|
||||||
|
|
||||||
|
if err == nil && jwtCookie.Value != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ja *JWTCookieSessionAuthenticator) Login(
|
||||||
|
user *User,
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
r *http.Request) (*User, error) {
|
||||||
|
|
||||||
|
jwtCookie, err := r.Cookie(ja.config.CookieName)
|
||||||
|
var rawtoken string
|
||||||
|
|
||||||
|
if err == nil && jwtCookie.Value != "" {
|
||||||
|
rawtoken = jwtCookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if t.Method != jwt.SigningMethodEdDSA {
|
||||||
|
return nil, errors.New("only Ed25519/EdDSA supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
|
||||||
|
if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer {
|
||||||
|
// The (unvalidated) issuer seems to be the expected one,
|
||||||
|
// use public cross login key from config
|
||||||
|
return ja.publicKeyCrossLogin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cross login key configured or issuer not expected
|
||||||
|
// Try own key
|
||||||
|
return ja.publicKey, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error while parsing token")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check token validity and extract paypload
|
||||||
|
if err := token.Claims.Valid(); err != nil {
|
||||||
|
log.Warn("jwt token claims are not valid")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
sub, _ := claims["sub"].(string)
|
||||||
|
exp, _ := claims["exp"].(float64)
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if val, ok := claims["name"]; ok {
|
||||||
|
name, _ = val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles []string
|
||||||
|
|
||||||
|
if ja.config.ForceJWTValidationViaDatabase {
|
||||||
|
// Deny any logins for unknown usernames
|
||||||
|
if user == nil {
|
||||||
|
log.Warn("Could not find user from JWT in internal database.")
|
||||||
|
return nil, errors.New("unknown user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take user roles from database instead of trusting the JWT
|
||||||
|
roles = user.Roles
|
||||||
|
} else {
|
||||||
|
// Extract roles from JWT (if present)
|
||||||
|
if rawroles, ok := claims["roles"].([]interface{}); ok {
|
||||||
|
for _, rr := range rawroles {
|
||||||
|
if r, ok := rr.(string); ok {
|
||||||
|
roles = append(roles, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Ask browser to) Delete JWT cookie
|
||||||
|
deletedCookie := &http.Cookie{
|
||||||
|
Name: ja.config.CookieName,
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: -1,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
http.SetCookie(rw, deletedCookie)
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
user = &User{
|
||||||
|
Username: sub,
|
||||||
|
Name: name,
|
||||||
|
Roles: roles,
|
||||||
|
AuthType: AuthSession,
|
||||||
|
AuthSource: AuthViaToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Expiration = time.Unix(int64(exp), 0)
|
||||||
|
return user, nil
|
||||||
|
}
|
103
internal/auth/jwtSession.go
Normal file
103
internal/auth/jwtSession.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
||||||
|
// All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWTSessionAuthenticator struct {
|
||||||
|
auth *Authentication
|
||||||
|
|
||||||
|
loginTokenKey []byte // HS256 key
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
|
||||||
|
|
||||||
|
func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
|
||||||
|
|
||||||
|
ja.auth = auth
|
||||||
|
|
||||||
|
if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
|
||||||
|
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not decode cross login JWT HS512 key")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ja.loginTokenKey = bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ja *JWTSessionAuthenticator) CanLogin(
|
||||||
|
user *User,
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
r *http.Request) bool {
|
||||||
|
|
||||||
|
return r.Header.Get("Authorization") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ja *JWTSessionAuthenticator) Login(
|
||||||
|
user *User,
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
r *http.Request) (*User, error) {
|
||||||
|
|
||||||
|
rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||||
|
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if t.Method == jwt.SigningMethodHS256 || t.Method == jwt.SigningMethodHS512 {
|
||||||
|
return ja.loginTokenKey, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("AUTH/JWT > unkown signing method for login token: %s (known: HS256, HS512, EdDSA)", t.Method.Alg())
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error while parsing jwt token")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = token.Claims.Valid(); err != nil {
|
||||||
|
log.Warn("jwt token claims are not valid")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
sub, _ := claims["sub"].(string)
|
||||||
|
exp, _ := claims["exp"].(float64)
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if val, ok := claims["name"]; ok {
|
||||||
|
name, _ = val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles []string
|
||||||
|
if rawroles, ok := claims["roles"]; ok {
|
||||||
|
for _, r := range rawroles.([]string) {
|
||||||
|
if isValidRole(r) {
|
||||||
|
roles = append(roles, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
user = &User{
|
||||||
|
Username: sub,
|
||||||
|
Name: name,
|
||||||
|
Roles: roles,
|
||||||
|
AuthType: AuthSession,
|
||||||
|
AuthSource: AuthViaToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Expiration = time.Unix(int64(exp), 0)
|
||||||
|
return user, nil
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
@ -93,13 +93,6 @@ func (la *LdapAuthenticator) Login(
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (la *LdapAuthenticator) Auth(
|
|
||||||
rw http.ResponseWriter,
|
|
||||||
r *http.Request) (*User, error) {
|
|
||||||
|
|
||||||
return la.auth.AuthViaSession(rw, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (la *LdapAuthenticator) Sync() error {
|
func (la *LdapAuthenticator) Sync() error {
|
||||||
|
|
||||||
const IN_DB int = 1
|
const IN_DB int = 1
|
||||||
|
@ -46,10 +46,3 @@ func (la *LocalAuthenticator) Login(
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (la *LocalAuthenticator) Auth(
|
|
||||||
rw http.ResponseWriter,
|
|
||||||
r *http.Request) (*User, error) {
|
|
||||||
|
|
||||||
return la.auth.AuthViaSession(rw, r)
|
|
||||||
}
|
|
||||||
|
165
internal/auth/roles.go
Normal file
165
internal/auth/roles.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
|
||||||
|
// All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Role int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleAnonymous Role = iota
|
||||||
|
RoleApi
|
||||||
|
RoleUser
|
||||||
|
RoleManager
|
||||||
|
RoleSupport
|
||||||
|
RoleAdmin
|
||||||
|
RoleError
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetRoleString(roleInt Role) string {
|
||||||
|
return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoleEnum(roleStr string) Role {
|
||||||
|
switch strings.ToLower(roleStr) {
|
||||||
|
case "admin":
|
||||||
|
return RoleAdmin
|
||||||
|
case "support":
|
||||||
|
return RoleSupport
|
||||||
|
case "manager":
|
||||||
|
return RoleManager
|
||||||
|
case "user":
|
||||||
|
return RoleUser
|
||||||
|
case "api":
|
||||||
|
return RoleApi
|
||||||
|
case "anonymous":
|
||||||
|
return RoleAnonymous
|
||||||
|
default:
|
||||||
|
return RoleError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidRole(role string) bool {
|
||||||
|
return getRoleEnum(role) != RoleError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
|
||||||
|
if isValidRole(role) {
|
||||||
|
for _, r := range u.Roles {
|
||||||
|
if r == role {
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) HasRole(role Role) bool {
|
||||||
|
for _, r := range u.Roles {
|
||||||
|
if r == GetRoleString(role) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role-Arrays are short: performance not impacted by nested loop
|
||||||
|
func (u *User) HasAnyRole(queryroles []Role) bool {
|
||||||
|
for _, ur := range u.Roles {
|
||||||
|
for _, qr := range queryroles {
|
||||||
|
if ur == GetRoleString(qr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role-Arrays are short: performance not impacted by nested loop
|
||||||
|
func (u *User) HasAllRoles(queryroles []Role) bool {
|
||||||
|
target := len(queryroles)
|
||||||
|
matches := 0
|
||||||
|
for _, ur := range u.Roles {
|
||||||
|
for _, qr := range queryroles {
|
||||||
|
if ur == GetRoleString(qr) {
|
||||||
|
matches += 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches == target {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role-Arrays are short: performance not impacted by nested loop
|
||||||
|
func (u *User) HasNotRoles(queryroles []Role) bool {
|
||||||
|
matches := 0
|
||||||
|
for _, ur := range u.Roles {
|
||||||
|
for _, qr := range queryroles {
|
||||||
|
if ur == GetRoleString(qr) {
|
||||||
|
matches += 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches == 0 {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by API endpoint '/roles/' from frontend: Only required for admin config -> Check Admin Role
|
||||||
|
func GetValidRoles(user *User) ([]string, error) {
|
||||||
|
var vals []string
|
||||||
|
if user.HasRole(RoleAdmin) {
|
||||||
|
for i := RoleApi; i < RoleError; i++ {
|
||||||
|
vals = append(vals, GetRoleString(i))
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by routerConfig web.page setup in backend: Only requires known user
|
||||||
|
func GetValidRolesMap(user *User) (map[string]Role, error) {
|
||||||
|
named := make(map[string]Role)
|
||||||
|
if user.HasNotRoles([]Role{RoleAnonymous}) {
|
||||||
|
for i := RoleApi; i < RoleError; i++ {
|
||||||
|
named[GetRoleString(i)] = i
|
||||||
|
}
|
||||||
|
return named, nil
|
||||||
|
}
|
||||||
|
return named, fmt.Errorf("only known users are allowed to fetch a list of roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find highest role
|
||||||
|
func (u *User) GetAuthLevel() Role {
|
||||||
|
if u.HasRole(RoleAdmin) {
|
||||||
|
return RoleAdmin
|
||||||
|
} else if u.HasRole(RoleSupport) {
|
||||||
|
return RoleSupport
|
||||||
|
} else if u.HasRole(RoleManager) {
|
||||||
|
return RoleManager
|
||||||
|
} else if u.HasRole(RoleUser) {
|
||||||
|
return RoleUser
|
||||||
|
} else if u.HasRole(RoleApi) {
|
||||||
|
return RoleApi
|
||||||
|
} else if u.HasRole(RoleAnonymous) {
|
||||||
|
return RoleAnonymous
|
||||||
|
} else {
|
||||||
|
return RoleError
|
||||||
|
}
|
||||||
|
}
|
BIN
internal/repository/testdata/job.db
vendored
BIN
internal/repository/testdata/job.db
vendored
Binary file not shown.
Loading…
Reference in New Issue
Block a user