mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-25 12:59:06 +01:00
Readd URL token and cleanup
Fix session values.
This commit is contained in:
parent
b8273a9b02
commit
19d645f65c
@ -1,11 +1,13 @@
|
|||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
ClusterCockpit uses JSON Web Tokens (JWT) for authorization of its APIs.
|
ClusterCockpit uses JSON Web Tokens (JWT) for authorization of its APIs. JSON
|
||||||
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
|
Web Token (JWT) is an open standard (RFC 7519) that defines a compact and
|
||||||
This information can be verified and trusted because it is digitally signed.
|
self-contained way for securely transmitting information between parties as a
|
||||||
In ClusterCockpit JWTs are signed using a public/private key pair using ECDSA.
|
JSON object. This information can be verified and trusted because it is
|
||||||
Because tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
|
digitally signed. In ClusterCockpit JWTs are signed using a public/private key
|
||||||
Currently JWT tokens in ClusterCockpit not yet expire.
|
pair using ECDSA. Because tokens are signed using public/private key pairs, the
|
||||||
|
signature also certifies that only the party holding the private key is the one
|
||||||
|
that signed it. Token expiration is set to the configuration option MaxAge.
|
||||||
|
|
||||||
## JWT Payload
|
## JWT Payload
|
||||||
|
|
||||||
@ -25,8 +27,15 @@ $ ./gen-keypair
|
|||||||
2. Add keypair in your `.env` file. A template can be found in `./configs`.
|
2. Add keypair in your `.env` file. A template can be found in `./configs`.
|
||||||
|
|
||||||
There are two usage scenarios:
|
There are two usage scenarios:
|
||||||
* The APIs are used during a browser session. In this case on login a JWT token is issued on login, that is used by the web frontend to authorize against the GraphQL and REST APIs.
|
* The APIs are used during a browser session. In this case on login a JWT token
|
||||||
* The REST API is used outside a browser session, e.g. by scripts. In this case you have to issue a token manually. This possible from within the configuration view or on the command line. It is recommended to issue a JWT token in this case for a special user that only has the `api` role. By using different users for different purposes a fine grained access control and access revocation management is possible.
|
is issued on login, that is used by the web frontend to authorize against the
|
||||||
|
GraphQL and REST APIs.
|
||||||
|
* The REST API is used outside a browser session, e.g. by scripts. In this case
|
||||||
|
you have to issue a token manually. This possible from within the
|
||||||
|
configuration view or on the command line. It is recommended to issue a JWT
|
||||||
|
token in this case for a special user that only has the `api` role. By using
|
||||||
|
different users for different purposes a fine grained access control and
|
||||||
|
access revocation management is possible.
|
||||||
|
|
||||||
The token is commonly specified in the Authorization HTTP header using the Bearer schema.
|
The token is commonly specified in the Authorization HTTP header using the Bearer schema.
|
||||||
|
|
||||||
@ -46,16 +55,24 @@ $ curl -X GET "<API ENDPOINT>" -H "accept: application/json" -H "Content-Type:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Accept externally generated JWTs provided via cookie
|
## Accept externally generated JWTs provided via cookie
|
||||||
If there is an external service like an AuthAPI that can generate JWTs and hand them over to ClusterCockpit via cookies, CC can be configured to accept them:
|
If there is an external service like an AuthAPI that can generate JWTs and hand
|
||||||
|
them over to ClusterCockpit via cookies, CC can be configured to accept them:
|
||||||
|
|
||||||
1. `.env`: CC needs a public ed25519 key to verify foreign JWT signatures. Public keys in PEM format can be converted with the instructions in [/tools/convert-pem-pubkey-for-cc](../tools/convert-pem-pubkey-for-cc/Readme.md) .
|
1. `.env`: CC needs a public ed25519 key to verify foreign JWT signatures.
|
||||||
|
Public keys in PEM format can be converted with the instructions in
|
||||||
|
[/tools/convert-pem-pubkey-for-cc](../tools/convert-pem-pubkey-for-cc/Readme.md)
|
||||||
|
.
|
||||||
|
|
||||||
```
|
```
|
||||||
CROSS_LOGIN_JWT_PUBLIC_KEY="+51iXX8BdLFocrppRxIw52xCOf8xFSH/eNilN5IHVGc="
|
CROSS_LOGIN_JWT_PUBLIC_KEY="+51iXX8BdLFocrppRxIw52xCOf8xFSH/eNilN5IHVGc="
|
||||||
```
|
```
|
||||||
|
|
||||||
2. `config.json`: Insert a name for the cookie (set by the external service) containing the JWT so that CC knows where to look at. Define a trusted issuer (JWT claim 'iss'), otherwise it will be rejected.
|
2. `config.json`: Insert a name for the cookie (set by the external service)
|
||||||
If you want usernames and user roles from JWTs ('sub' and 'roles' claim) to be validated against CC's internal database, you need to enable it here. Unknown users will then be rejected and roles set via JWT will be ignored.
|
containing the JWT so that CC knows where to look at. Define a trusted issuer
|
||||||
|
(JWT claim 'iss'), otherwise it will be rejected. If you want usernames and
|
||||||
|
user roles from JWTs ('sub' and 'roles' claim) to be validated against CC's
|
||||||
|
internal database, you need to enable it here. Unknown users will then be
|
||||||
|
rejected and roles set via JWT will be ignored.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"jwts": {
|
"jwts": {
|
||||||
@ -65,7 +82,8 @@ If you want usernames and user roles from JWTs ('sub' and 'roles' claim) to be v
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Make sure your external service includes the same issuer (`iss`) in its JWTs. Example JWT payload:
|
3. Make sure your external service includes the same issuer (`iss`) in its JWTs.
|
||||||
|
Example JWT payload:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -97,26 +97,29 @@ func (auth *Authentication) AuthViaSession(
|
|||||||
if session.IsNew {
|
if session.IsNew {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
//
|
||||||
var username string
|
// var username string
|
||||||
var projects, roles []string
|
// var projects, roles []string
|
||||||
|
//
|
||||||
if val, ok := session.Values["username"]; ok {
|
// if val, ok := session.Values["username"]; ok {
|
||||||
username, _ = val.(string)
|
// username, _ = val.(string)
|
||||||
} else {
|
// } else {
|
||||||
return nil, errors.New("No key username in session")
|
// return nil, errors.New("no key username in session")
|
||||||
}
|
// }
|
||||||
if val, ok := session.Values["projects"]; ok {
|
// if val, ok := session.Values["projects"]; ok {
|
||||||
projects, _ = val.([]string)
|
// projects, _ = val.([]string)
|
||||||
} else {
|
// } else {
|
||||||
return nil, errors.New("No key projects in session")
|
// return nil, errors.New("no key projects in session")
|
||||||
}
|
// }
|
||||||
if val, ok := session.Values["projects"]; ok {
|
// if val, ok := session.Values["projects"]; ok {
|
||||||
roles, _ = val.([]string)
|
// roles, _ = val.([]string)
|
||||||
} else {
|
// } else {
|
||||||
return nil, errors.New("No key roles in session")
|
// return nil, errors.New("no key roles in session")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
username, _ := session.Values["username"].(string)
|
||||||
|
projects, _ := session.Values["projects"].([]string)
|
||||||
|
roles, _ := session.Values["roles"].([]string)
|
||||||
return &User{
|
return &User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Projects: projects,
|
Projects: projects,
|
||||||
@ -261,6 +264,12 @@ func (auth *Authentication) Auth(
|
|||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("authentication failed: %s", err.Error())
|
||||||
|
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
user, err = auth.AuthViaSession(rw, r)
|
user, err = auth.AuthViaSession(rw, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -276,7 +285,7 @@ func (auth *Authentication) Auth(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("authentication failed: no authenticator applied")
|
log.Debug("authentication failed")
|
||||||
onfailure(rw, r, errors.New("unauthorized (please login first)"))
|
onfailure(rw, r, errors.New("unauthorized (please login first)"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,11 @@ func (ja *JWTAuthenticator) AuthViaJWT(
|
|||||||
rawtoken = strings.TrimPrefix(rawtoken, "Bearer ")
|
rawtoken = strings.TrimPrefix(rawtoken, "Bearer ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// there is no token
|
||||||
|
if rawtoken == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
@ -81,6 +86,11 @@ func (ja *JWTAuthenticator) AuthViaJWT(
|
|||||||
// Token is valid, extract payload
|
// Token is valid, extract payload
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
sub, _ := claims["sub"].(string)
|
sub, _ := claims["sub"].(string)
|
||||||
|
exp, _ := claims["exp"].(float64)
|
||||||
|
|
||||||
|
if exp < float64(time.Now().Unix()) {
|
||||||
|
return nil, errors.New("token is expired")
|
||||||
|
}
|
||||||
|
|
||||||
var roles []string
|
var roles []string
|
||||||
|
|
||||||
@ -109,8 +119,8 @@ func (ja *JWTAuthenticator) AuthViaJWT(
|
|||||||
return &User{
|
return &User{
|
||||||
Username: sub,
|
Username: sub,
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
AuthType: AuthSession,
|
AuthType: AuthToken,
|
||||||
AuthSource: AuthViaToken,
|
AuthSource: -1,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +24,6 @@ type JWTCookieSessionAuthenticator struct {
|
|||||||
privateKey ed25519.PrivateKey
|
privateKey ed25519.PrivateKey
|
||||||
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
|
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
|
||||||
|
|
||||||
loginTokenKey []byte // HS256 key
|
|
||||||
|
|
||||||
config *schema.JWTAuthConfig
|
config *schema.JWTAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,15 +53,6 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
|
|||||||
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
|
// Look for external public keys
|
||||||
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
|
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
|
||||||
if keyFound && pubKeyCrossLogin != "" {
|
if keyFound && pubKeyCrossLogin != "" {
|
||||||
@ -105,13 +94,6 @@ func (ja *JWTCookieSessionAuthenticator) CanLogin(
|
|||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) bool {
|
r *http.Request) bool {
|
||||||
|
|
||||||
if ja.publicKeyCrossLogin == nil ||
|
|
||||||
ja.config == nil ||
|
|
||||||
ja.config.TrustedExternalIssuer == "" {
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieName := ""
|
cookieName := ""
|
||||||
if ja.config != nil && ja.config.CookieName != "" {
|
if ja.config != nil && ja.config.CookieName != "" {
|
||||||
cookieName = ja.config.CookieName
|
cookieName = ja.config.CookieName
|
||||||
|
@ -45,7 +45,7 @@ func (ja *JWTSessionAuthenticator) CanLogin(
|
|||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) bool {
|
r *http.Request) bool {
|
||||||
|
|
||||||
return r.Header.Get("Authorization") != ""
|
return r.Header.Get("Authorization") != "" || r.URL.Query().Get("login-token") != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ja *JWTSessionAuthenticator) Login(
|
func (ja *JWTSessionAuthenticator) Login(
|
||||||
@ -54,6 +54,10 @@ func (ja *JWTSessionAuthenticator) Login(
|
|||||||
r *http.Request) (*User, error) {
|
r *http.Request) (*User, error) {
|
||||||
|
|
||||||
rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
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) {
|
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
||||||
if t.Method == jwt.SigningMethodHS256 || t.Method == jwt.SigningMethodHS512 {
|
if t.Method == jwt.SigningMethodHS256 || t.Method == jwt.SigningMethodHS512 {
|
||||||
return ja.loginTokenKey, nil
|
return ja.loginTokenKey, nil
|
||||||
|
@ -39,7 +39,8 @@ func (la *LocalAuthenticator) Login(
|
|||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) (*User, error) {
|
r *http.Request) (*User, error) {
|
||||||
|
|
||||||
if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.FormValue("password"))); e != nil {
|
if e := bcrypt.CompareHashAndPassword([]byte(user.Password),
|
||||||
|
[]byte(r.FormValue("password"))); e != nil {
|
||||||
log.Errorf("AUTH/LOCAL > Authentication for user %s failed!", user.Username)
|
log.Errorf("AUTH/LOCAL > Authentication for user %s failed!", user.Username)
|
||||||
return nil, fmt.Errorf("AUTH/LOCAL > Authentication failed")
|
return nil, fmt.Errorf("AUTH/LOCAL > Authentication failed")
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ type LdapConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JWTAuthConfig struct {
|
type JWTAuthConfig struct {
|
||||||
// Specifies for how long a session or JWT shall be valid
|
// Specifies for how long a JWT token shall be valid
|
||||||
// as a string parsable by time.ParseDuration().
|
// as a string parsable by time.ParseDuration().
|
||||||
MaxAge int64 `json:"max-age"`
|
MaxAge int64 `json:"max-age"`
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ type ProgramConfig struct {
|
|||||||
LdapConfig *LdapConfig `json:"ldap"`
|
LdapConfig *LdapConfig `json:"ldap"`
|
||||||
JwtConfig *JWTAuthConfig `json:"jwts"`
|
JwtConfig *JWTAuthConfig `json:"jwts"`
|
||||||
|
|
||||||
// If 0 or empty, the session/token does not expire!
|
// If 0 or empty, the session does not expire!
|
||||||
SessionMaxAge string `json:"session-max-age"`
|
SessionMaxAge string `json:"session-max-age"`
|
||||||
|
|
||||||
// If both those options are not empty, use HTTPS using those certificates.
|
// If both those options are not empty, use HTTPS using those certificates.
|
||||||
@ -113,7 +113,7 @@ type ProgramConfig struct {
|
|||||||
// redirect every request incoming at port 80 to that url.
|
// redirect every request incoming at port 80 to that url.
|
||||||
RedirectHttpTo string `json:"redirect-http-to"`
|
RedirectHttpTo string `json:"redirect-http-to"`
|
||||||
|
|
||||||
// If overwriten, at least all the options in the defaults below must
|
// If overwritten, at least all the options in the defaults below must
|
||||||
// be provided! Most options here can be overwritten by the user.
|
// be provided! Most options here can be overwritten by the user.
|
||||||
UiDefaults map[string]interface{} `json:"ui-defaults"`
|
UiDefaults map[string]interface{} `json:"ui-defaults"`
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user