mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-25 12:59:06 +01:00
Refactor auth and add docs
Cleanup and reformat
This commit is contained in:
parent
e7ecc260f8
commit
df9fd77d06
156
docs/dev-authentication.md
Normal file
156
docs/dev-authentication.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Overview
|
||||
|
||||
The implementation of authentication is not easy to understand by just looking
|
||||
at the code. The authentication is implemented in `internal/auth/`. In `auth.go`
|
||||
an interface is defined that any authentication provider must fullfil. It also
|
||||
acts as a dispatcher to delegate the calls to the available authentication
|
||||
providers.
|
||||
|
||||
The most important routine are:
|
||||
* `CanLogin()` Check if the authentication method is supported for login attempt
|
||||
* `Login()` Handle POST request to login user and start a new session
|
||||
* `Auth()` Authenticate user and put User Object in context of the request
|
||||
|
||||
The http router calls auth in the following cases:
|
||||
* `r.Handle("/login", authentication.Login( ... )).Methods(http.MethodPost)`:
|
||||
The POST request on the `/login` route will call the Login callback.
|
||||
* Any route in the secured subrouter will always call Auth(), on success it will
|
||||
call the next handler in the chain, on falure it will render the login
|
||||
template.
|
||||
```
|
||||
secured.Use(func(next http.Handler) http.Handler {
|
||||
return authentication.Auth(
|
||||
// On success;
|
||||
next,
|
||||
|
||||
// On failure:
|
||||
func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
// Render login form
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
For non API routes a JWT token can be used to initate an authenticated user
|
||||
session. This can either happen by calling the login/ route with a token
|
||||
provided in a header or the query URL or via the `Auth()` method on first access
|
||||
to a secured URL via aspecial cookie containing the JWT token.
|
||||
For API routes the access is authenticated on every request using the JWT token
|
||||
and no session is initiated.
|
||||
|
||||
# Login
|
||||
|
||||
The Login function (located in `auth.go`):
|
||||
* Extracts the user name and gets the user from the user database table. In case the
|
||||
user is not found the user obejct is set to nil.
|
||||
* Iterates over all authenticators and:
|
||||
- Calls the `CanLogin` function which checks if the authentication method is
|
||||
supported for this user and the user object is valid.
|
||||
- Calls the `Login` function to authenticate the user. On success a valid user
|
||||
object is returned.
|
||||
- Creates a new session object, stores the user attributes in the session and
|
||||
saves the session.
|
||||
- Calls the `onSuccess` http handler
|
||||
|
||||
## Local authenticator
|
||||
|
||||
This authenticator is applied if
|
||||
```
|
||||
return user != nil && user.AuthSource == AuthViaLocalPassword
|
||||
```
|
||||
|
||||
Compares the password provided by the login form to the password hash stored in
|
||||
the user database table:
|
||||
```
|
||||
if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.FormValue("password"))); e != nil {
|
||||
log.Errorf("AUTH/LOCAL > Authentication for user %s failed!", user.Username)
|
||||
return nil, fmt.Errorf("AUTH/LOCAL > Authentication failed")
|
||||
}
|
||||
```
|
||||
|
||||
## LDAP authenticator
|
||||
|
||||
This authenticator is applied if
|
||||
```
|
||||
return user != nil && user.AuthSource == AuthViaLDAP
|
||||
```
|
||||
|
||||
Gets the LDAP connection and tries a bind with the provided credentials:
|
||||
```
|
||||
if err := l.Bind(userDn, r.FormValue("password")); err != nil {
|
||||
log.Errorf("AUTH/LOCAL > Authentication for user %s failed: %v", user.Username, err)
|
||||
return nil, fmt.Errorf("AUTH/LDAP > Authentication failed")
|
||||
}
|
||||
```
|
||||
|
||||
## JWT authenticator
|
||||
|
||||
Login via JWT token will create a session without password.
|
||||
For login the `X-Auth-Token` header is not supported.
|
||||
This authenticator is applied if either user is not nil and auth source is
|
||||
`AuthViaToken` or the Authorization header is present or the URL query key
|
||||
login-token is present:
|
||||
```
|
||||
return (user != nil && user.AuthSource == AuthViaToken) ||
|
||||
r.Header.Get("Authorization") != "" ||
|
||||
r.URL.Query().Get("login-token") != ""
|
||||
```
|
||||
|
||||
The Login function:
|
||||
* Parses the token
|
||||
* Check if the signing method is EdDSA or HS256 or HS512
|
||||
* Check if claims are valid and extracts the claims
|
||||
* The following claims have to be present:
|
||||
- `sub`: The subject, in this case this is the username
|
||||
- `exp`: Expiration in Unix epoch time
|
||||
- `roles`: String array with roles of user
|
||||
* In case user is not yet set, which is usually the case:
|
||||
- Try to fetch user from database
|
||||
- In case user is not yet present add user to user databse table with `AuthViaToken` AuthSource.
|
||||
* Return valid user object
|
||||
|
||||
# Auth
|
||||
|
||||
The Auth function (located in `auth.go`):
|
||||
* Returns a new http handler function that is defined right away
|
||||
* This handler iterates over all authenticators
|
||||
* Calls `Auth()` on every authenticator
|
||||
* If err is not nil and the user object is valid it puts the user object in the
|
||||
request context and starts the onsuccess http handler
|
||||
* Otherwise it calls the onfailure handler
|
||||
|
||||
## Local
|
||||
|
||||
Calls the `AuthViaSession()` function in `auth.go`. This will extract username,
|
||||
projects and roles from the session and initialize a user object with those
|
||||
values.
|
||||
|
||||
## LDAP
|
||||
|
||||
Calls the `AuthViaSession()` function in `auth.go`. This will extract username,
|
||||
projects and roles from the session and initialize a user object with those
|
||||
values.
|
||||
|
||||
# JWT
|
||||
|
||||
Check for JWT token:
|
||||
* Is token passed in the `X-Auth-Token` or `Authorization` header
|
||||
* If no token is found in a header it tries to read the token from a configured
|
||||
cookie.
|
||||
|
||||
Finally it calls AuthViaSession in `auth.go` if a valid session exists. This is
|
||||
true if a JWT token was previously used to initiate a session. In this case the
|
||||
user object initialized with the session is returned rightaway.
|
||||
|
||||
In case a token was found extract and parse the token:
|
||||
* Check if signing method is Ed25519/EdDSA
|
||||
* In case publicKeyCrossLogin is configured:
|
||||
- Check if `iss` issuer claim matched trusted issuer from configuration
|
||||
- Return public cross login key
|
||||
- Otherwise return standard public key
|
||||
* Check if claims are valid
|
||||
* Depending on the option `ForceJWTValidationViaDatabase ` the roles are
|
||||
extracted from JWT token or taken from user object fetched from database
|
||||
* In case the token was extracted from cookie create a new session and ask the
|
||||
browser to delete the JWT cookie
|
||||
* Return valid user object
|
||||
|
@ -75,10 +75,7 @@ func getRoleEnum(roleStr string) Role {
|
||||
}
|
||||
|
||||
func isValidRole(role string) bool {
|
||||
if getRoleEnum(role) == RoleError {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return getRoleEnum(role) != RoleError
|
||||
}
|
||||
|
||||
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
|
||||
@ -175,7 +172,7 @@ func GetValidRolesMap(user *User) (map[string]Role, error) {
|
||||
}
|
||||
return named, nil
|
||||
}
|
||||
return named, fmt.Errorf("Only known users are allowed to fetch a list of roles")
|
||||
return named, fmt.Errorf("only known users are allowed to fetch a list of roles")
|
||||
}
|
||||
|
||||
// Find highest role
|
||||
@ -300,6 +297,7 @@ func (auth *Authentication) AuthViaSession(
|
||||
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)
|
||||
@ -320,11 +318,9 @@ func (auth *Authentication) Login(
|
||||
err := errors.New("no authenticator applied")
|
||||
username := r.FormValue("username")
|
||||
user := (*User)(nil)
|
||||
|
||||
if username != "" {
|
||||
if user, _ = auth.GetUser(username); err != nil {
|
||||
// log.Warnf("login of unkown user %v", username)
|
||||
_ = err
|
||||
}
|
||||
user, _ = auth.GetUser(username)
|
||||
}
|
||||
|
||||
for _, authenticator := range auth.authenticators {
|
||||
|
@ -103,7 +103,9 @@ func (ja *JWTAuthenticator) CanLogin(
|
||||
rw http.ResponseWriter,
|
||||
r *http.Request) bool {
|
||||
|
||||
return (user != nil && user.AuthSource == AuthViaToken) || r.Header.Get("Authorization") != "" || r.URL.Query().Get("login-token") != ""
|
||||
return (user != nil && user.AuthSource == AuthViaToken) ||
|
||||
r.Header.Get("Authorization") != "" ||
|
||||
r.URL.Query().Get("login-token") != ""
|
||||
}
|
||||
|
||||
func (ja *JWTAuthenticator) Login(
|
||||
@ -111,13 +113,9 @@ func (ja *JWTAuthenticator) Login(
|
||||
rw http.ResponseWriter,
|
||||
r *http.Request) (*User, error) {
|
||||
|
||||
rawtoken := r.Header.Get("X-Auth-Token")
|
||||
rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
if rawtoken == "" {
|
||||
rawtoken = r.Header.Get("Authorization")
|
||||
rawtoken = strings.TrimPrefix(rawtoken, "Bearer ")
|
||||
if rawtoken == "" {
|
||||
rawtoken = r.URL.Query().Get("login-token")
|
||||
}
|
||||
rawtoken = r.URL.Query().Get("login-token")
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
||||
@ -134,7 +132,7 @@ func (ja *JWTAuthenticator) Login(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := token.Claims.Valid(); err != nil {
|
||||
if err = token.Claims.Valid(); err != nil {
|
||||
log.Warn("jwt token claims are not valid")
|
||||
return nil, err
|
||||
}
|
||||
@ -220,7 +218,10 @@ func (ja *JWTAuthenticator) Auth(
|
||||
}
|
||||
|
||||
// Is there more than one public key?
|
||||
if ja.publicKeyCrossLogin != nil && ja.config != nil && ja.config.TrustedExternalIssuer != "" {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user