feat: add long-lived api tokens (#1085)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/1085 Co-authored-by: konrad <k@knt.li> Co-committed-by: konrad <k@knt.li>
This commit is contained in:
parent
2598550e49
commit
1322cb16d7
11 changed files with 51 additions and 8 deletions
|
@ -6,6 +6,10 @@ service:
|
||||||
# The duration of the issed JWT tokens in seconds.
|
# The duration of the issed JWT tokens in seconds.
|
||||||
# The default is 259200 seconds (3 Days).
|
# The default is 259200 seconds (3 Days).
|
||||||
jwtttl: 259200
|
jwtttl: 259200
|
||||||
|
# The duration of the "remember me" time in seconds. When the login request is made with
|
||||||
|
# the long param set, the token returned will be valid for this period.
|
||||||
|
# The default is 2592000 seconds (30 Days).
|
||||||
|
jwtttllong: 2592000
|
||||||
# The interface on which to run the webserver
|
# The interface on which to run the webserver
|
||||||
interface: ":3456"
|
interface: ":3456"
|
||||||
# Path to Unix socket. If set, it will be created and used instead of tcp
|
# Path to Unix socket. If set, it will be created and used instead of tcp
|
||||||
|
|
|
@ -91,6 +91,19 @@ Full path: `service.jwtttl`
|
||||||
Environment path: `VIKUNJA_SERVICE_JWTTTL`
|
Environment path: `VIKUNJA_SERVICE_JWTTTL`
|
||||||
|
|
||||||
|
|
||||||
|
### jwtttllong
|
||||||
|
|
||||||
|
The duration of the "remember me" time in seconds. When the login request is made with
|
||||||
|
the long param set, the token returned will be valid for this period.
|
||||||
|
The default is 2592000 seconds (30 Days).
|
||||||
|
|
||||||
|
Default: `2592000`
|
||||||
|
|
||||||
|
Full path: `service.jwtttllong`
|
||||||
|
|
||||||
|
Environment path: `VIKUNJA_SERVICE_JWTTTLLONG`
|
||||||
|
|
||||||
|
|
||||||
### interface
|
### interface
|
||||||
|
|
||||||
The interface on which to run the webserver
|
The interface on which to run the webserver
|
||||||
|
|
|
@ -38,6 +38,7 @@ const (
|
||||||
// #nosec
|
// #nosec
|
||||||
ServiceJWTSecret Key = `service.JWTSecret`
|
ServiceJWTSecret Key = `service.JWTSecret`
|
||||||
ServiceJWTTTL Key = `service.jwtttl`
|
ServiceJWTTTL Key = `service.jwtttl`
|
||||||
|
ServiceJWTTTLLong Key = `service.jwtttllong`
|
||||||
ServiceInterface Key = `service.interface`
|
ServiceInterface Key = `service.interface`
|
||||||
ServiceUnixSocket Key = `service.unixsocket`
|
ServiceUnixSocket Key = `service.unixsocket`
|
||||||
ServiceUnixSocketMode Key = `service.unixsocketmode`
|
ServiceUnixSocketMode Key = `service.unixsocketmode`
|
||||||
|
@ -227,7 +228,8 @@ func InitDefaultConfig() {
|
||||||
|
|
||||||
// Service
|
// Service
|
||||||
ServiceJWTSecret.setDefault(random)
|
ServiceJWTSecret.setDefault(random)
|
||||||
ServiceJWTTTL.setDefault(259200)
|
ServiceJWTTTL.setDefault(259200) // 72 hours
|
||||||
|
ServiceJWTTTLLong.setDefault(2592000) // 30 days
|
||||||
ServiceInterface.setDefault(":3456")
|
ServiceInterface.setDefault(":3456")
|
||||||
ServiceUnixSocket.setDefault("")
|
ServiceUnixSocket.setDefault("")
|
||||||
ServiceFrontendurl.setDefault("")
|
ServiceFrontendurl.setDefault("")
|
||||||
|
|
|
@ -119,7 +119,7 @@ func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context)
|
||||||
|
|
||||||
func addUserTokenToContext(t *testing.T, user *user.User, c echo.Context) {
|
func addUserTokenToContext(t *testing.T, user *user.User, c echo.Context) {
|
||||||
// Get the token as a string
|
// Get the token as a string
|
||||||
token, err := auth.NewUserJWTAuthtoken(user)
|
token, err := auth.NewUserJWTAuthtoken(user, false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// We send the string token through the parsing function to get a valid jwt.Token
|
// We send the string token through the parsing function to get a valid jwt.Token
|
||||||
tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
|
|
@ -42,8 +42,8 @@ type Token struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserAuthTokenResponse creates a new user auth token response from a user object.
|
// NewUserAuthTokenResponse creates a new user auth token response from a user object.
|
||||||
func NewUserAuthTokenResponse(u *user.User, c echo.Context) error {
|
func NewUserAuthTokenResponse(u *user.User, c echo.Context, long bool) error {
|
||||||
t, err := NewUserJWTAuthtoken(u)
|
t, err := NewUserJWTAuthtoken(u, long)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -52,10 +52,13 @@ func NewUserAuthTokenResponse(u *user.User, c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserJWTAuthtoken generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests.
|
// NewUserJWTAuthtoken generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests.
|
||||||
func NewUserJWTAuthtoken(u *user.User) (token string, err error) {
|
func NewUserJWTAuthtoken(u *user.User, long bool) (token string, err error) {
|
||||||
t := jwt.New(jwt.SigningMethodHS256)
|
t := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
|
||||||
var ttl = time.Duration(config.ServiceJWTTTL.GetInt64())
|
var ttl = time.Duration(config.ServiceJWTTTL.GetInt64())
|
||||||
|
if long {
|
||||||
|
ttl = time.Duration(config.ServiceJWTTTLLong.GetInt64())
|
||||||
|
}
|
||||||
var exp = time.Now().Add(time.Second * ttl).Unix()
|
var exp = time.Now().Add(time.Second * ttl).Unix()
|
||||||
|
|
||||||
// Set claims
|
// Set claims
|
||||||
|
@ -68,6 +71,7 @@ func NewUserJWTAuthtoken(u *user.User) (token string, err error) {
|
||||||
claims["name"] = u.Name
|
claims["name"] = u.Name
|
||||||
claims["emailRemindersEnabled"] = u.EmailRemindersEnabled
|
claims["emailRemindersEnabled"] = u.EmailRemindersEnabled
|
||||||
claims["isLocalUser"] = u.Issuer == user.IssuerLocal
|
claims["isLocalUser"] = u.Issuer == user.IssuerLocal
|
||||||
|
claims["long"] = long
|
||||||
|
|
||||||
// Generate encoded token and send it as response.
|
// Generate encoded token and send it as response.
|
||||||
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
|
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
|
||||||
|
|
|
@ -198,7 +198,7 @@ func HandleCallback(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create token
|
// Create token
|
||||||
return auth.NewUserAuthTokenResponse(u, c)
|
return auth.NewUserAuthTokenResponse(u, c, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *user.User, err error) {
|
func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *user.User, err error) {
|
||||||
|
|
|
@ -102,7 +102,7 @@ func Login(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create token
|
// Create token
|
||||||
return auth.NewUserAuthTokenResponse(user, c)
|
return auth.NewUserAuthTokenResponse(user, c, u.LongToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenewToken gives a new token to every user with a valid token
|
// RenewToken gives a new token to every user with a valid token
|
||||||
|
@ -156,6 +156,12 @@ func RenewToken(c echo.Context) (err error) {
|
||||||
return handler.HandleHTTPError(err, c)
|
return handler.HandleHTTPError(err, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create token
|
var long bool
|
||||||
return auth.NewUserAuthTokenResponse(user, c)
|
lng, has := claims["long"]
|
||||||
|
if has {
|
||||||
|
long = lng.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token
|
||||||
|
return auth.NewUserAuthTokenResponse(user, c, long)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8847,6 +8847,10 @@ var doc = `{
|
||||||
"user.Login": {
|
"user.Login": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"long_token": {
|
||||||
|
"description": "If true, the token returned will be valid a lot longer than default. Useful for \"remember me\" style logins.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"description": "The password for the user.",
|
"description": "The password for the user.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
@ -8831,6 +8831,10 @@
|
||||||
"user.Login": {
|
"user.Login": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"long_token": {
|
||||||
|
"description": "If true, the token returned will be valid a lot longer than default. Useful for \"remember me\" style logins.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"description": "The password for the user.",
|
"description": "The password for the user.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
@ -1157,6 +1157,10 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
user.Login:
|
user.Login:
|
||||||
properties:
|
properties:
|
||||||
|
long_token:
|
||||||
|
description: If true, the token returned will be valid a lot longer than default.
|
||||||
|
Useful for "remember me" style logins.
|
||||||
|
type: boolean
|
||||||
password:
|
password:
|
||||||
description: The password for the user.
|
description: The password for the user.
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -44,6 +44,8 @@ type Login struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
// The totp passcode of a user. Only needs to be provided when enabled.
|
// The totp passcode of a user. Only needs to be provided when enabled.
|
||||||
TOTPPasscode string `json:"totp_passcode"`
|
TOTPPasscode string `json:"totp_passcode"`
|
||||||
|
// If true, the token returned will be valid a lot longer than default. Useful for "remember me" style logins.
|
||||||
|
LongToken bool `json:"long_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status int
|
type Status int
|
||||||
|
|
Loading…
Reference in a new issue