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:
konrad 2022-02-06 13:18:08 +00:00
parent 2598550e49
commit 1322cb16d7
11 changed files with 51 additions and 8 deletions

View file

@ -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

View file

@ -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

View file

@ -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("")

View file

@ -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) {

View file

@ -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()))

View file

@ -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) {

View file

@ -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)
} }

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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