Authentication with OpenID Connect providers (#713)

Add config docs

Lint

Move provider-related stuff to separate file

Refactor getting auth providers

Fix tests

Fix user tests

Fix openid tests

Add swagger docs

Fix lint

Fix lint issues

Fix checking if the user already exists

Make sure to create a new namespace for new users

Docs

Add tests for openid

Remove unnessecary err check

Consistently return nil users if creating a new user failed

Move sending confirmation email to separate function

Better variable names

Move checks to separate functions

Refactor creating user into seperate file

Fix creating new local users

Test creating new users from different issuers

Generate a random username right away if no preferred username has been given

Add todo

Cache openid providers

Add getting int clientids

Fix migration

Move creating tokens to auth package

Add getting or creating a third party user

Add parsing claims

Add retreiving auth tokens

Add token callback from openid package

Add check for provider key

Add routes

Start adding openid auth handler

Add config for openid auth

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/713
Co-Authored-By: konrad <konrad@kola-entertainments.de>
Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2020-11-21 16:38:58 +00:00
parent f67fe2ce25
commit 2b5c9ae7a8
37 changed files with 1265 additions and 178 deletions

View file

@ -215,3 +215,32 @@ legal:
keyvalue: keyvalue:
# The type of the storage backend. Can be either "memory" or "redis". If "redis" is chosen it needs to be configured seperately. # The type of the storage backend. Can be either "memory" or "redis". If "redis" is chosen it needs to be configured seperately.
type: "memory" type: "memory"
auth:
# Local authentication will let users log in and register (if enabled) through the db.
# This is the default auth mechanism and does not require any additional configuration.
local:
# Enable or disable local authentication
enabled: true
# OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
# The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
# **Note:** The frontend expects to be redirected after authentication by the third party
# to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url with your third party
# auth service accordingy if you're using the default vikunja frontend.
# Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) for more information about how to configure openid authentication.
openid:
# Enable or disable OpenID Connect authentication
enabled: false
# The url to redirect clients to. Defaults to the configured frontend url. If you're using Vikunja with the official
# frontend, you don't need to change this value.
redirecturl: <frontend url>
# A list of enabled providers
providers:
# The name of the provider as it will appear in the frontend.
- name:
# The auth url to send users to if they want to authenticate using OpenID Connect.
authurl:
# The client ID used to authenticate Vikunja at the OpenID Connect provider.
clientid:
# The client secret used to authenticate Vikunja at the OpenID Connect provider.
clientsecret:

View file

@ -563,3 +563,27 @@ The type of the storage backend. Can be either "memory" or "redis". If "redis" i
Default: `memory` Default: `memory`
---
## auth
### local
Local authentication will let users log in and register (if enabled) through the db.
This is the default auth mechanism and does not require any additional configuration.
Default: `<empty>`
### openid
OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
**Note:** The frontend expects to be redirected after authentication by the third party
to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url with your third party
auth service accordingy if you're using the default vikunja frontend.
Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) for more information about how to configure openid authentication.
Default: `<empty>`

5
go.mod
View file

@ -26,10 +26,12 @@ require (
github.com/beevik/etree v1.1.0 // indirect github.com/beevik/etree v1.1.0 // indirect
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
github.com/client9/misspell v0.3.4 github.com/client9/misspell v0.3.4
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/cweill/gotests v1.5.3 github.com/cweill/gotests v1.5.3
github.com/d4l3k/messagediff v1.2.1 // indirect github.com/d4l3k/messagediff v1.2.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
github.com/fzipp/gocyclo v0.3.1 github.com/fzipp/gocyclo v0.3.1
github.com/gabriel-vasile/mimetype v1.1.2 github.com/gabriel-vasile/mimetype v1.1.2
github.com/getsentry/sentry-go v0.8.0 github.com/getsentry/sentry-go v0.8.0
@ -59,6 +61,7 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml v1.8.0 // indirect github.com/pelletier/go-toml v1.8.0 // indirect
github.com/pquerna/otp v1.3.0 github.com/pquerna/otp v1.3.0
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
github.com/prometheus/client_golang v1.8.0 github.com/prometheus/client_golang v1.8.0
github.com/samedi/caldav-go v3.0.0+incompatible github.com/samedi/caldav-go v3.0.0+incompatible
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
@ -75,6 +78,7 @@ require (
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
golang.org/x/lint v0.0.0-20200302205851-738671d3881b golang.org/x/lint v0.0.0-20200302205851-738671d3881b
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 // indirect golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 // indirect golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
@ -82,6 +86,7 @@ require (
gopkg.in/d4l3k/messagediff.v1 v1.2.1 gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.57.0 // indirect gopkg.in/ini.v1 v1.57.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c
honnef.co/go/tools v0.0.1-2020.1.5 honnef.co/go/tools v0.0.1-2020.1.5
src.techknowlogick.com/xgo v1.1.1-0.20200811225412-bff6512e7c9c src.techknowlogick.com/xgo v1.1.1-0.20200811225412-bff6512e7c9c

8
go.sum
View file

@ -109,6 +109,8 @@ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcju
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
@ -143,6 +145,8 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@ -642,6 +646,8 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e h1:BLqxdwZ6j771IpSCRx7s/GJjXHUE00Hmu7/YegCGdzA=
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ=
github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
@ -1140,6 +1146,8 @@ gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuv
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=

View file

@ -26,6 +26,7 @@ import (
"code.vikunja.io/api/pkg/initialize" "code.vikunja.io/api/pkg/initialize"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -157,11 +158,16 @@ var userCreateCmd = &cobra.Command{
Email: userFlagEmail, Email: userFlagEmail,
Password: getPasswordFromFlagOrInput(), Password: getPasswordFromFlagOrInput(),
} }
_, err := user.CreateUser(u) newUser, err := user.CreateUser(u)
if err != nil { if err != nil {
log.Fatalf("Error creating new user: %s", err) log.Fatalf("Error creating new user: %s", err)
} }
err = models.CreateNewNamespaceForUser(newUser)
if err != nil {
log.Fatalf("Error creating new namespace for user: %s", err)
}
fmt.Printf("\nUser was created successfully.\n") fmt.Printf("\nUser was created successfully.\n")
}, },
} }

View file

@ -52,6 +52,11 @@ const (
ServiceEnableTotp Key = `service.enabletotp` ServiceEnableTotp Key = `service.enabletotp`
ServiceSentryDsn Key = `service.sentrydsn` ServiceSentryDsn Key = `service.sentrydsn`
AuthLocalEnabled Key = `auth.local.enabled`
AuthOpenIDEnabled Key = `auth.openid.enabled`
AuthOpenIDRedirectURL Key = `auth.openid.redirecturl`
AuthOpenIDProviders Key = `auth.openid.providers`
LegalImprintURL Key = `legal.imprinturl` LegalImprintURL Key = `legal.imprinturl`
LegalPrivacyURL Key = `legal.privacyurl` LegalPrivacyURL Key = `legal.privacyurl`
@ -158,6 +163,11 @@ func (k Key) GetStringSlice() []string {
return viper.GetStringSlice(string(k)) return viper.GetStringSlice(string(k))
} }
// Get returns the raw value from a config option
func (k Key) Get() interface{} {
return viper.Get(string(k))
}
var timezone *time.Location var timezone *time.Location
// GetTimeZone returns the time zone configured for vikunja // GetTimeZone returns the time zone configured for vikunja
@ -216,6 +226,10 @@ func InitDefaultConfig() {
ServiceEnableTaskComments.setDefault(true) ServiceEnableTaskComments.setDefault(true)
ServiceEnableTotp.setDefault(true) ServiceEnableTotp.setDefault(true)
// Auth
AuthLocalEnabled.setDefault(true)
AuthOpenIDEnabled.setDefault(false)
// Database // Database
DatabaseType.setDefault("sqlite") DatabaseType.setDefault("sqlite")
DatabaseHost.setDefault("localhost") DatabaseHost.setDefault("localhost")
@ -322,6 +336,10 @@ func InitConfig() {
RateLimitStore.Set(KeyvalueType.GetString()) RateLimitStore.Set(KeyvalueType.GetString())
} }
if AuthOpenIDRedirectURL.GetString() == "" {
AuthOpenIDRedirectURL.Set(ServiceFrontendurl.GetString() + "auth/openid/")
}
log.Printf("Using config file: %s", viper.ConfigFileUsed()) log.Printf("Using config file: %s", viper.ConfigFileUsed())
} }

View file

@ -4,6 +4,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user1@example.com' email: 'user1@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- -
@ -11,6 +12,7 @@
username: 'user2' username: 'user2'
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user2@example.com' email: 'user2@example.com'
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- -
@ -19,6 +21,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user3@example.com' email: 'user3@example.com'
password_reset_token: passwordresettesttoken password_reset_token: passwordresettesttoken
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- -
@ -27,6 +30,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user4@example.com' email: 'user4@example.com'
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- -
@ -36,6 +40,7 @@
email: 'user5@example.com' email: 'user5@example.com'
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
is_active: false is_active: false
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
# This use is used to create a whole bunch of lists which are then shared directly with a user # This use is used to create a whole bunch of lists which are then shared directly with a user
@ -44,6 +49,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user6@example.com' email: 'user6@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 7 - id: 7
@ -51,6 +57,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user7@example.com' email: 'user7@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 8 - id: 8
@ -58,6 +65,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user8@example.com' email: 'user8@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 9 - id: 9
@ -65,6 +73,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user9@example.com' email: 'user9@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 10 - id: 10
@ -72,6 +81,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user10@example.com' email: 'user10@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 11 - id: 11
@ -79,6 +89,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user11@example.com' email: 'user11@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 12 - id: 12
@ -86,6 +97,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user12@example.com' email: 'user12@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 13 - id: 13
@ -93,5 +105,15 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user14@example.com' email: 'user14@example.com'
is_active: true is_active: true
issuer: local
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 14
username: 'user14'
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user15@some.service.com'
is_active: true
issuer: 'https://some.service.com'
subject: '12345'
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12

View file

@ -28,8 +28,8 @@ import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/routes" "code.vikunja.io/api/pkg/routes"
v1 "code.vikunja.io/api/pkg/routes/api/v1"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
@ -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 := v1.NewUserJWTAuthtoken(user) token, err := auth.NewUserJWTAuthtoken(user)
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) {
@ -131,7 +131,7 @@ func addUserTokenToContext(t *testing.T, user *user.User, c echo.Context) {
func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.Context) { func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.Context) {
// Get the token as a string // Get the token as a string
token, err := v1.NewLinkShareJWTAuthtoken(share) token, err := auth.NewLinkShareJWTAuthtoken(share)
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

@ -0,0 +1,50 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type user20201025195822 struct {
Issuer string `xorm:"text null" json:"-"`
Subject string `xorm:"text null" json:"-"`
}
func (user20201025195822) TableName() string {
return "users"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20201025195822",
Description: "",
Migrate: func(tx *xorm.Engine) error {
err := tx.Sync2(user20201025195822{})
if err != nil {
return err
}
_, err = tx.Cols("issuer").Update(&user20201025195822{Issuer: "local"})
return err
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View file

@ -56,6 +56,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
ID: 2, ID: 2,
Username: "user2", Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View file

@ -52,6 +52,7 @@ func TestLabel_ReadAll(t *testing.T) {
Username: "user1", Username: "user1",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -99,6 +100,7 @@ func TestLabel_ReadAll(t *testing.T) {
ID: 2, ID: 2,
Username: "user2", Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },
@ -159,6 +161,7 @@ func TestLabel_ReadOne(t *testing.T) {
Username: "user1", Username: "user1",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -217,6 +220,7 @@ func TestLabel_ReadOne(t *testing.T) {
ID: 2, ID: 2,
Username: "user2", Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View file

@ -176,6 +176,7 @@ func TestListUser_ReadAll(t *testing.T) {
Username: "user1", Username: "user1",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },
@ -186,6 +187,7 @@ func TestListUser_ReadAll(t *testing.T) {
ID: 2, ID: 2,
Username: "user2", Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View file

@ -445,6 +445,16 @@ func (n *Namespace) Create(a web.Auth) (err error) {
return return
} }
// CreateNewNamespaceForUser creates a new namespace for a user. To prevent import cycles, we can't do that
// directly in the user.Create function.
func CreateNewNamespaceForUser(user *user.User) (err error) {
newN := &Namespace{
Title: user.Username,
Description: user.Username + "'s namespace.",
}
return newN.Create(user)
}
// Delete deletes a namespace // Delete deletes a namespace
// @Summary Deletes a namespace // @Summary Deletes a namespace
// @Description Delets a namespace // @Description Delets a namespace

View file

@ -175,6 +175,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
Username: "user1", Username: "user1",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },
@ -185,6 +186,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
ID: 2, ID: 2,
Username: "user2", Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View file

@ -35,6 +35,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Username: "user1", Username: "user1",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -42,6 +43,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
ID: 2, ID: 2,
Username: "user2", Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -49,6 +51,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
ID: 6, ID: 6,
Username: "user6", Username: "user6",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
IsActive: true, IsActive: true,
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,

View file

@ -31,6 +31,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user1", Username: "user1",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -38,6 +39,7 @@ func TestListUsersFromList(t *testing.T) {
ID: 2, ID: 2,
Username: "user2", Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -46,6 +48,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user3", Username: "user3",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
PasswordResetToken: "passwordresettesttoken", PasswordResetToken: "passwordresettesttoken",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -55,6 +58,7 @@ func TestListUsersFromList(t *testing.T) {
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: false, IsActive: false,
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael", EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -64,6 +68,7 @@ func TestListUsersFromList(t *testing.T) {
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: false, IsActive: false,
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael", EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -72,6 +77,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user6", Username: "user6",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -80,6 +86,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user7", Username: "user7",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -88,6 +95,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user8", Username: "user8",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -96,6 +104,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user9", Username: "user9",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -104,6 +113,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user10", Username: "user10",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -112,6 +122,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user11", Username: "user11",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -120,6 +131,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user12", Username: "user12",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -128,6 +140,7 @@ func TestListUsersFromList(t *testing.T) {
Username: "user13", Username: "user13",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true, IsActive: true,
Issuer: "local",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
package v1 package auth
import ( import (
"net/http" "net/http"
@ -35,6 +35,21 @@ const (
AuthTypeLinkShare AuthTypeLinkShare
) )
// Token represents an authentification token
type Token struct {
Token string `json:"token"`
}
// NewUserAuthTokenResponse creates a new user auth token response from a user object.
func NewUserAuthTokenResponse(u *user.User, c echo.Context) error {
t, err := NewUserJWTAuthtoken(u)
if err != nil {
return err
}
return c.JSON(http.StatusOK, Token{Token: t})
}
// 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(user *user.User) (token string, err error) { func NewUserJWTAuthtoken(user *user.User) (token string, err error) {
t := jwt.New(jwt.SigningMethodHS256) t := jwt.New(jwt.SigningMethodHS256)

View file

@ -0,0 +1,34 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package openid
import (
"os"
"testing"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
)
// TestMain is the main test function used to bootstrap the test env
func TestMain(m *testing.M) {
user.InitTests()
files.InitTests()
models.SetupTests()
os.Exit(m.Run())
}

View file

@ -0,0 +1,206 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package openid
import (
"context"
"encoding/json"
"math/rand"
"net/http"
"time"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/user"
"github.com/coreos/go-oidc"
petname "github.com/dustinkirkland/golang-petname"
"github.com/labstack/echo/v4"
"golang.org/x/oauth2"
)
// Callback contains the callback after an auth request was made and redirected
type Callback struct {
Code string `query:"code" json:"code"`
Scope string `query:"scop" json:"scope"`
}
// Provider is the structure of an OpenID Connect provider
type Provider struct {
Name string `json:"name"`
Key string `json:"key"`
AuthURL string `json:"auth_url"`
ClientID string `json:"client_id"`
ClientSecret string `json:"-"`
OpenIDProvider *oidc.Provider `json:"-"`
Oauth2Config *oauth2.Config `json:"-"`
}
type claims struct {
Email string `json:"email"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
// HandleCallback handles the auth request callback after redirecting from the provider with an auth code
// @Summary Authenticate a user with OpenID Connect
// @Description After a redirect from the OpenID Connect provider to the frontend has been made with the authentication `code`, this endpoint can be used to obtain a jwt token for that user and thus log them in.
// @tags auth
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param callback body openid.Callback true "The openid callback"
// @Param provider path int true "The OpenID Connect provider key as returned by the /info endpoint"
// @Success 200 {object} auth.Token
// @Failure 500 {object} models.Message "Internal error"
// @Router /auth/openid/{provider}/callback [post]
func HandleCallback(c echo.Context) error {
cb := &Callback{}
if err := c.Bind(cb); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{Message: "Bad data"})
}
// Check if the provider exists
providerKey := c.Param("provider")
provider, err := GetProvider(providerKey)
if err != nil {
log.Error(err)
return err
}
if provider == nil {
return c.JSON(http.StatusBadRequest, models.Message{Message: "Provider does not exist"})
}
// Parse the access & ID token
oauth2Token, err := provider.Oauth2Config.Exchange(context.Background(), cb.Code)
if err != nil {
if rerr, is := err.(*oauth2.RetrieveError); is {
log.Error(err)
details := make(map[string]interface{})
if err := json.Unmarshal(rerr.Body, &details); err != nil {
return err
}
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"message": "Could not authenticate against third party.",
"details": details,
})
}
return err
}
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return c.JSON(http.StatusBadRequest, models.Message{Message: "Missing token"})
}
verifier := provider.OpenIDProvider.Verifier(&oidc.Config{ClientID: provider.ClientID})
// Parse and verify ID Token payload.
idToken, err := verifier.Verify(context.Background(), rawIDToken)
if err != nil {
return err
}
// Extract custom claims
cl := &claims{}
err = idToken.Claims(cl)
if err != nil {
return err
}
// Check if we have seen this user before
u, err := getOrCreateUser(cl, idToken.Issuer, idToken.Subject)
if err != nil {
return err
}
// Create token
return auth.NewUserAuthTokenResponse(u, c)
}
func getOrCreateUser(cl *claims, issuer, subject string) (u *user.User, err error) {
// Check if the user exists for that issuer and subject
u, err = user.GetUserWithEmail(&user.User{
Issuer: issuer,
Subject: subject,
})
if err != nil && !user.IsErrUserDoesNotExist(err) {
return nil, err
}
// If no user exists, create one with the preferred username if it is not already taken
if user.IsErrUserDoesNotExist(err) {
uu := &user.User{
Username: cl.PreferredUsername,
Email: cl.Email,
IsActive: true,
Issuer: issuer,
Subject: subject,
}
// Check if we actually have a preferred username and generate a random one right away if we don't
if uu.Username == "" {
uu.Username = petname.Generate(3, "-")
}
u, err = user.CreateUser(uu)
if err != nil && !user.IsErrUsernameExists(err) {
return nil, err
}
// If their preferred username is already taken, create some random one from the email and subject
if user.IsErrUsernameExists(err) {
uu.Username = petname.Generate(3, "-")
u, err = user.CreateUser(uu)
if err != nil {
return nil, err
}
}
// And create its namespace
err = models.CreateNewNamespaceForUser(u)
if err != nil {
return nil, err
}
return
}
// If it exists, check if the email address changed and change it if not
if cl.Email != u.Email {
u.Email = cl.Email
u, err = user.UpdateUser(&user.User{
ID: u.ID,
Email: cl.Email,
Issuer: issuer,
Subject: subject,
})
if err != nil {
return nil, err
}
}
return
}

View file

@ -0,0 +1,75 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package openid
import (
"testing"
"code.vikunja.io/api/pkg/db"
"github.com/stretchr/testify/assert"
)
func TestGetOrCreateUser(t *testing.T) {
t.Run("new user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
cl := &claims{
Email: "test@example.com",
PreferredUsername: "someUserWhoDoesNotExistYet",
}
u, err := getOrCreateUser(cl, "https://some.issuer", "12345")
assert.NoError(t, err)
db.AssertExists(t, "users", map[string]interface{}{
"id": u.ID,
"email": cl.Email,
"username": "someUserWhoDoesNotExistYet",
}, false)
})
t.Run("new user, no username provided", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
cl := &claims{
Email: "test@example.com",
PreferredUsername: "",
}
u, err := getOrCreateUser(cl, "https://some.issuer", "12345")
assert.NoError(t, err)
assert.NotEmpty(t, u.Username)
db.AssertExists(t, "users", map[string]interface{}{
"id": u.ID,
"email": cl.Email,
}, false)
})
t.Run("new user, no email address", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
cl := &claims{
Email: "",
}
_, err := getOrCreateUser(cl, "https://some.issuer", "12345")
assert.Error(t, err)
})
t.Run("existing user, different email address", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
cl := &claims{
Email: "other-email-address@some.service.com",
}
u, err := getOrCreateUser(cl, "https://some.service.com", "12345")
assert.NoError(t, err)
db.AssertExists(t, "users", map[string]interface{}{
"id": u.ID,
"email": cl.Email,
}, false)
})
}

View file

@ -0,0 +1,127 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package openid
import (
"context"
"regexp"
"strconv"
"strings"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/modules/keyvalue"
kerr "code.vikunja.io/api/pkg/modules/keyvalue/error"
"github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
// GetAllProviders returns all configured providers
func GetAllProviders() (providers []*Provider, err error) {
ps, err := keyvalue.Get("openid_providers")
if err != nil && kerr.IsErrValueNotFoundForKey(err) {
rawProvider := config.AuthOpenIDProviders.Get().([]interface{})
for _, p := range rawProvider {
pi := p.(map[interface{}]interface{})
provider, err := getProviderFromMap(pi)
if err != nil {
return nil, err
}
providers = append(providers, provider)
k := getKeyFromName(pi["name"].(string))
err = keyvalue.Put("openid_provider_"+k, provider)
if err != nil {
return nil, err
}
}
err = keyvalue.Put("openid_providers", providers)
}
if ps != nil {
return ps.([]*Provider), nil
}
return
}
// GetProvider retrieves a provider from keyvalue
func GetProvider(key string) (provider *Provider, err error) {
var p interface{}
p, err = keyvalue.Get("openid_provider_" + key)
if err != nil && kerr.IsErrValueNotFoundForKey(err) {
_, err = GetAllProviders() // This will put all providers in cache
if err != nil {
return nil, err
}
p, err = keyvalue.Get("openid_provider_" + key)
}
if p != nil {
return p.(*Provider), nil
}
return nil, err
}
func getKeyFromName(name string) string {
reg := regexp.MustCompile("[^a-z0-9]+")
return reg.ReplaceAllString(strings.ToLower(name), "")
}
func getProviderFromMap(pi map[interface{}]interface{}) (*Provider, error) {
k := getKeyFromName(pi["name"].(string))
provider := &Provider{
Name: pi["name"].(string),
Key: k,
AuthURL: pi["authurl"].(string),
ClientSecret: pi["clientsecret"].(string),
}
cl, is := pi["clientid"].(int)
if is {
provider.ClientID = strconv.Itoa(cl)
} else {
provider.ClientID = pi["clientid"].(string)
}
var err error
provider.OpenIDProvider, err = oidc.NewProvider(context.Background(), provider.AuthURL)
if err != nil {
return nil, err
}
provider.Oauth2Config = &oauth2.Config{
ClientID: provider.ClientID,
ClientSecret: provider.ClientSecret,
RedirectURL: config.AuthOpenIDRedirectURL.GetString() + k,
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.OpenIDProvider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
provider.AuthURL = provider.Oauth2Config.Endpoint.AuthURL
return provider, nil
}

View file

@ -25,9 +25,9 @@ import (
"code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/modules/background" "code.vikunja.io/api/pkg/modules/background"
"code.vikunja.io/api/pkg/modules/background/unsplash" "code.vikunja.io/api/pkg/modules/background/unsplash"
v1 "code.vikunja.io/api/pkg/routes/api/v1"
"code.vikunja.io/web" "code.vikunja.io/web"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
@ -69,7 +69,7 @@ func (bp *BackgroundProvider) SearchBackgrounds(c echo.Context) error {
// This function does all kinds of preparations for setting and uploading a background // This function does all kinds of preparations for setting and uploading a background
func (bp *BackgroundProvider) setBackgroundPreparations(c echo.Context) (list *models.List, auth web.Auth, err error) { func (bp *BackgroundProvider) setBackgroundPreparations(c echo.Context) (list *models.List, auth web.Auth, err error) {
auth, err = v1.GetAuthFromClaims(c) auth, err = auth2.GetAuthFromClaims(c)
if err != nil { if err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error()) return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error())
} }
@ -180,7 +180,7 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
// @Router /lists/{id}/background [get] // @Router /lists/{id}/background [get]
func GetListBackground(c echo.Context) error { func GetListBackground(c echo.Context) error {
auth, err := v1.GetAuthFromClaims(c) auth, err := auth2.GetAuthFromClaims(c)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error()) return echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error())
} }

View file

@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/modules/auth/openid"
"code.vikunja.io/api/pkg/modules/migration/todoist" "code.vikunja.io/api/pkg/modules/migration/todoist"
"code.vikunja.io/api/pkg/modules/migration/wunderlist" "code.vikunja.io/api/pkg/modules/migration/wunderlist"
"code.vikunja.io/api/pkg/version" "code.vikunja.io/api/pkg/version"
@ -39,6 +40,22 @@ type vikunjaInfos struct {
TotpEnabled bool `json:"totp_enabled"` TotpEnabled bool `json:"totp_enabled"`
Legal legalInfo `json:"legal"` Legal legalInfo `json:"legal"`
CaldavEnabled bool `json:"caldav_enabled"` CaldavEnabled bool `json:"caldav_enabled"`
AuthInfo authInfo `json:"auth"`
}
type authInfo struct {
Local localAuthInfo `json:"local"`
OpenIDConnect openIDAuthInfo `json:"openid_connect"`
}
type localAuthInfo struct {
Enabled bool `json:"enabled"`
}
type openIDAuthInfo struct {
Enabled bool `json:"enabled"`
RedirectURL string `json:"redirect_url"`
Providers []*openid.Provider `json:"providers"`
} }
type legalInfo struct { type legalInfo struct {
@ -68,8 +85,24 @@ func Info(c echo.Context) error {
ImprintURL: config.LegalImprintURL.GetString(), ImprintURL: config.LegalImprintURL.GetString(),
PrivacyPolicyURL: config.LegalPrivacyURL.GetString(), PrivacyPolicyURL: config.LegalPrivacyURL.GetString(),
}, },
AuthInfo: authInfo{
Local: localAuthInfo{
Enabled: config.AuthLocalEnabled.GetBool(),
},
OpenIDConnect: openIDAuthInfo{
Enabled: config.AuthOpenIDEnabled.GetBool(),
RedirectURL: config.AuthOpenIDRedirectURL.GetString(),
},
},
} }
providers, err := openid.GetAllProviders()
if err != nil {
return err
}
info.AuthInfo.OpenIDConnect.Providers = providers
// Migrators // Migrators
if config.MigrationWunderlistEnable.GetBool() { if config.MigrationWunderlistEnable.GetBool() {
m := &wunderlist.Migration{} m := &wunderlist.Migration{}

View file

@ -20,13 +20,14 @@ import (
"net/http" "net/http"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// LinkShareToken represents a link share auth token with extra infos about the actual link share // LinkShareToken represents a link share auth token with extra infos about the actual link share
type LinkShareToken struct { type LinkShareToken struct {
Token auth.Token
*models.LinkSharing *models.LinkSharing
ListID int64 `json:"list_id"` ListID int64 `json:"list_id"`
} }
@ -38,7 +39,7 @@ type LinkShareToken struct {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param share path string true "The share hash" // @Param share path string true "The share hash"
// @Success 200 {object} v1.Token "The valid jwt auth token." // @Success 200 {object} auth.Token "The valid jwt auth token."
// @Failure 400 {object} web.HTTPError "Invalid link share object provided." // @Failure 400 {object} web.HTTPError "Invalid link share object provided."
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /shares/{share}/auth [post] // @Router /shares/{share}/auth [post]
@ -49,13 +50,13 @@ func AuthenticateLinkShare(c echo.Context) error {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }
t, err := NewLinkShareJWTAuthtoken(share) t, err := auth.NewLinkShareJWTAuthtoken(share)
if err != nil { if err != nil {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }
return c.JSON(http.StatusOK, LinkShareToken{ return c.JSON(http.StatusOK, LinkShareToken{
Token: Token{Token: t}, Token: auth.Token{Token: t},
LinkSharing: share, LinkSharing: share,
ListID: share.ListID, ListID: share.ListID,
}) })

View file

@ -20,17 +20,13 @@ import (
"net/http" "net/http"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
user2 "code.vikunja.io/api/pkg/user" user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// Token represents an authentification token
type Token struct {
Token string `json:"token"`
}
// Login is the login handler // Login is the login handler
// @Summary Login // @Summary Login
// @Description Logs a user in. Returns a JWT-Token to authenticate further requests. // @Description Logs a user in. Returns a JWT-Token to authenticate further requests.
@ -38,7 +34,7 @@ type Token struct {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param credentials body user.Login true "The login credentials" // @Param credentials body user.Login true "The login credentials"
// @Success 200 {object} v1.Token // @Success 200 {object} auth.Token
// @Failure 400 {object} models.Message "Invalid user password model." // @Failure 400 {object} models.Message "Invalid user password model."
// @Failure 412 {object} models.Message "Invalid totp passcode." // @Failure 412 {object} models.Message "Invalid totp passcode."
// @Failure 403 {object} models.Message "Invalid username or password." // @Failure 403 {object} models.Message "Invalid username or password."
@ -71,12 +67,7 @@ func Login(c echo.Context) error {
} }
// Create token // Create token
t, err := NewUserJWTAuthtoken(user) return auth.NewUserAuthTokenResponse(user, c)
if err != nil {
return err
}
return c.JSON(http.StatusOK, Token{Token: t})
} }
// RenewToken gives a new token to every user with a valid token // RenewToken gives a new token to every user with a valid token
@ -86,7 +77,7 @@ func Login(c echo.Context) error {
// @tags user // @tags user
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} v1.Token // @Success 200 {object} auth.Token
// @Failure 400 {object} models.Message "Only user token are available for renew." // @Failure 400 {object} models.Message "Only user token are available for renew."
// @Router /user/token [post] // @Router /user/token [post]
func RenewToken(c echo.Context) (err error) { func RenewToken(c echo.Context) (err error) {
@ -94,18 +85,18 @@ func RenewToken(c echo.Context) (err error) {
jwtinf := c.Get("user").(*jwt.Token) jwtinf := c.Get("user").(*jwt.Token)
claims := jwtinf.Claims.(jwt.MapClaims) claims := jwtinf.Claims.(jwt.MapClaims)
typ := int(claims["type"].(float64)) typ := int(claims["type"].(float64))
if typ == AuthTypeLinkShare { if typ == auth.AuthTypeLinkShare {
share := &models.LinkSharing{} share := &models.LinkSharing{}
share.ID = int64(claims["id"].(float64)) share.ID = int64(claims["id"].(float64))
err := share.ReadOne() err := share.ReadOne()
if err != nil { if err != nil {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }
t, err := NewLinkShareJWTAuthtoken(share) t, err := auth.NewLinkShareJWTAuthtoken(share)
if err != nil { if err != nil {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }
return c.JSON(http.StatusOK, Token{Token: t}) return c.JSON(http.StatusOK, auth.Token{Token: t})
} }
user, err := user2.GetUserFromClaims(claims) user, err := user2.GetUserFromClaims(claims)
@ -114,10 +105,5 @@ func RenewToken(c echo.Context) (err error) {
} }
// Create token // Create token
t, err := NewUserJWTAuthtoken(user) return auth.NewUserAuthTokenResponse(user, c)
if err != nil {
return err
}
return c.JSON(http.StatusOK, Token{Token: t})
} }

View file

@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -46,7 +47,7 @@ func UploadTaskAttachment(c echo.Context) error {
} }
// Rights check // Rights check
auth, err := GetAuthFromClaims(c) auth, err := auth2.GetAuthFromClaims(c)
if err != nil { if err != nil {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }
@ -116,7 +117,7 @@ func GetTaskAttachment(c echo.Context) error {
} }
// Rights check // Rights check
auth, err := GetAuthFromClaims(c) auth, err := auth2.GetAuthFromClaims(c)
if err != nil { if err != nil {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }

View file

@ -21,6 +21,7 @@ import (
"strconv" "strconv"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -74,7 +75,7 @@ func ListUsersForList(c echo.Context) error {
} }
list := models.List{ID: listID} list := models.List{ID: listID}
auth, err := GetAuthFromClaims(c) auth, err := auth2.GetAuthFromClaims(c)
if err != nil { if err != nil {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }

View file

@ -57,8 +57,7 @@ func RegisterUser(c echo.Context) error {
} }
// Add its namespace // Add its namespace
newN := &models.Namespace{Title: newUser.Username, Description: newUser.Username + "'s namespace.", Owner: newUser} err = models.CreateNewNamespaceForUser(newUser)
err = newN.Create(newUser)
if err != nil { if err != nil {
return handler.HandleHTTPError(err, c) return handler.HandleHTTPError(err, c)
} }

View file

@ -22,7 +22,7 @@ import (
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
v1 "code.vikunja.io/api/pkg/routes/api/v1" auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@ -95,7 +95,7 @@ func setupMetricsMiddleware(a *echo.Group) {
// updateActiveUsersFromContext updates the currently active users in redis // updateActiveUsersFromContext updates the currently active users in redis
func updateActiveUsersFromContext(c echo.Context) (err error) { func updateActiveUsersFromContext(c echo.Context) (err error) {
auth, err := v1.GetAuthFromClaims(c) auth, err := auth2.GetAuthFromClaims(c)
if err != nil { if err != nil {
return return
} }

View file

@ -24,8 +24,8 @@ import (
"code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/red" "code.vikunja.io/api/pkg/red"
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3"
"github.com/ulule/limiter/v3/drivers/store/memory" "github.com/ulule/limiter/v3/drivers/store/memory"
@ -41,7 +41,7 @@ func RateLimit(rateLimiter *limiter.Limiter, rateLimitKind string) echo.Middlewa
case "ip": case "ip":
rateLimitKey = c.RealIP() rateLimitKey = c.RealIP()
case "user": case "user":
auth, err := apiv1.GetAuthFromClaims(c) auth, err := auth2.GetAuthFromClaims(c)
if err != nil { if err != nil {
log.Errorf("Error getting auth from jwt claims: %v", err) log.Errorf("Error getting auth from jwt claims: %v", err)
} }

View file

@ -53,6 +53,8 @@ import (
"code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/modules/auth/openid"
"code.vikunja.io/api/pkg/modules/background" "code.vikunja.io/api/pkg/modules/background"
backgroundHandler "code.vikunja.io/api/pkg/modules/background/handler" backgroundHandler "code.vikunja.io/api/pkg/modules/background/handler"
"code.vikunja.io/api/pkg/modules/background/unsplash" "code.vikunja.io/api/pkg/modules/background/unsplash"
@ -165,7 +167,7 @@ func NewEcho() *echo.Echo {
// Handler config // Handler config
handler.SetAuthProvider(&web.Auths{ handler.SetAuthProvider(&web.Auths{
AuthObject: apiv1.GetAuthFromClaims, AuthObject: auth.GetAuthFromClaims,
}) })
handler.SetLoggingProvider(log.GetLogger()) handler.SetLoggingProvider(log.GetLogger())
handler.SetMaxItemsPerPage(config.ServiceMaxItemsPerPage.GetInt()) handler.SetMaxItemsPerPage(config.ServiceMaxItemsPerPage.GetInt())
@ -220,12 +222,18 @@ func registerAPIRoutes(a *echo.Group) {
// Prometheus endpoint // Prometheus endpoint
setupMetrics(n) setupMetrics(n)
// User stuff if config.AuthLocalEnabled.GetBool() {
n.POST("/login", apiv1.Login) // User stuff
n.POST("/register", apiv1.RegisterUser) n.POST("/login", apiv1.Login)
n.POST("/user/password/token", apiv1.UserRequestResetPasswordToken) n.POST("/register", apiv1.RegisterUser)
n.POST("/user/password/reset", apiv1.UserResetPassword) n.POST("/user/password/token", apiv1.UserRequestResetPasswordToken)
n.POST("/user/confirm", apiv1.UserConfirmEmail) n.POST("/user/password/reset", apiv1.UserResetPassword)
n.POST("/user/confirm", apiv1.UserConfirmEmail)
}
if config.AuthOpenIDEnabled.GetBool() {
n.POST("/auth/openid/:provider/callback", openid.HandleCallback)
}
// Info endpoint // Info endpoint
n.GET("/info", apiv1.Info) n.GET("/info", apiv1.Info)

View file

@ -32,6 +32,58 @@ var doc = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/auth/openid/{provider}/callback": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "After a redirect from the OpenID Connect provider to the frontend has been made with the authentication ` + "`" + `code` + "`" + `, this endpoint can be used to obtain a jwt token for that user and thus log them in.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Authenticate a user with OpenID Connect",
"parameters": [
{
"description": "The openid callback",
"name": "callback",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/openid.Callback"
}
},
{
"type": "integer",
"description": "The OpenID Connect provider key as returned by the /info endpoint",
"name": "provider",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/auth.Token"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/backgrounds/unsplash/image/{image}": { "/backgrounds/unsplash/image/{image}": {
"get": { "get": {
"security": [ "security": [
@ -2426,7 +2478,7 @@ var doc = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/v1.Token" "$ref": "#/definitions/auth.Token"
} }
}, },
"400": { "400": {
@ -3671,7 +3723,7 @@ var doc = `{
"200": { "200": {
"description": "The valid jwt auth token.", "description": "The valid jwt auth token.",
"schema": { "schema": {
"$ref": "#/definitions/v1.Token" "$ref": "#/definitions/auth.Token"
} }
}, },
"400": { "400": {
@ -6240,7 +6292,7 @@ var doc = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/v1.Token" "$ref": "#/definitions/auth.Token"
} }
}, },
"400": { "400": {
@ -6352,6 +6404,14 @@ var doc = `{
} }
}, },
"definitions": { "definitions": {
"auth.Token": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
},
"background.Image": { "background.Image": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7430,6 +7490,34 @@ var doc = `{
} }
} }
}, },
"openid.Callback": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"scope": {
"type": "string"
}
}
},
"openid.Provider": {
"type": "object",
"properties": {
"auth_url": {
"type": "string"
},
"client_id": {
"type": "string"
},
"key": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"todoist.Migration": { "todoist.Migration": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7577,14 +7665,6 @@ var doc = `{
} }
} }
}, },
"v1.Token": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
},
"v1.UserAvatarProvider": { "v1.UserAvatarProvider": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7604,6 +7684,17 @@ var doc = `{
} }
} }
}, },
"v1.authInfo": {
"type": "object",
"properties": {
"local": {
"$ref": "#/definitions/v1.localAuthInfo"
},
"openid_connect": {
"$ref": "#/definitions/v1.openIDAuthInfo"
}
}
},
"v1.legalInfo": { "v1.legalInfo": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7615,9 +7706,37 @@ var doc = `{
} }
} }
}, },
"v1.localAuthInfo": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"v1.openIDAuthInfo": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"providers": {
"type": "array",
"items": {
"$ref": "#/definitions/openid.Provider"
}
},
"redirect_url": {
"type": "string"
}
}
},
"v1.vikunjaInfos": { "v1.vikunjaInfos": {
"type": "object", "type": "object",
"properties": { "properties": {
"auth": {
"$ref": "#/definitions/v1.authInfo"
},
"available_migrators": { "available_migrators": {
"type": "array", "type": "array",
"items": { "items": {

View file

@ -15,6 +15,58 @@
}, },
"basePath": "/api/v1", "basePath": "/api/v1",
"paths": { "paths": {
"/auth/openid/{provider}/callback": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "After a redirect from the OpenID Connect provider to the frontend has been made with the authentication `code`, this endpoint can be used to obtain a jwt token for that user and thus log them in.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Authenticate a user with OpenID Connect",
"parameters": [
{
"description": "The openid callback",
"name": "callback",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/openid.Callback"
}
},
{
"type": "integer",
"description": "The OpenID Connect provider key as returned by the /info endpoint",
"name": "provider",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/auth.Token"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/backgrounds/unsplash/image/{image}": { "/backgrounds/unsplash/image/{image}": {
"get": { "get": {
"security": [ "security": [
@ -2409,7 +2461,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/v1.Token" "$ref": "#/definitions/auth.Token"
} }
}, },
"400": { "400": {
@ -3654,7 +3706,7 @@
"200": { "200": {
"description": "The valid jwt auth token.", "description": "The valid jwt auth token.",
"schema": { "schema": {
"$ref": "#/definitions/v1.Token" "$ref": "#/definitions/auth.Token"
} }
}, },
"400": { "400": {
@ -6223,7 +6275,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/v1.Token" "$ref": "#/definitions/auth.Token"
} }
}, },
"400": { "400": {
@ -6335,6 +6387,14 @@
} }
}, },
"definitions": { "definitions": {
"auth.Token": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
},
"background.Image": { "background.Image": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7413,6 +7473,34 @@
} }
} }
}, },
"openid.Callback": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"scope": {
"type": "string"
}
}
},
"openid.Provider": {
"type": "object",
"properties": {
"auth_url": {
"type": "string"
},
"client_id": {
"type": "string"
},
"key": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"todoist.Migration": { "todoist.Migration": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7560,14 +7648,6 @@
} }
} }
}, },
"v1.Token": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
},
"v1.UserAvatarProvider": { "v1.UserAvatarProvider": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7587,6 +7667,17 @@
} }
} }
}, },
"v1.authInfo": {
"type": "object",
"properties": {
"local": {
"$ref": "#/definitions/v1.localAuthInfo"
},
"openid_connect": {
"$ref": "#/definitions/v1.openIDAuthInfo"
}
}
},
"v1.legalInfo": { "v1.legalInfo": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7598,9 +7689,37 @@
} }
} }
}, },
"v1.localAuthInfo": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"v1.openIDAuthInfo": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"providers": {
"type": "array",
"items": {
"$ref": "#/definitions/openid.Provider"
}
},
"redirect_url": {
"type": "string"
}
}
},
"v1.vikunjaInfos": { "v1.vikunjaInfos": {
"type": "object", "type": "object",
"properties": { "properties": {
"auth": {
"$ref": "#/definitions/v1.authInfo"
},
"available_migrators": { "available_migrators": {
"type": "array", "type": "array",
"items": { "items": {

View file

@ -1,5 +1,10 @@
basePath: /api/v1 basePath: /api/v1
definitions: definitions:
auth.Token:
properties:
token:
type: string
type: object
background.Image: background.Image:
properties: properties:
id: id:
@ -795,6 +800,24 @@ definitions:
minLength: 1 minLength: 1
type: string type: string
type: object type: object
openid.Callback:
properties:
code:
type: string
scope:
type: string
type: object
openid.Provider:
properties:
auth_url:
type: string
client_id:
type: string
key:
type: string
name:
type: string
type: object
todoist.Migration: todoist.Migration:
properties: properties:
code: code:
@ -899,11 +922,6 @@ definitions:
minLength: 1 minLength: 1
type: string type: string
type: object type: object
v1.Token:
properties:
token:
type: string
type: object
v1.UserAvatarProvider: v1.UserAvatarProvider:
properties: properties:
avatar_provider: avatar_provider:
@ -916,6 +934,13 @@ definitions:
old_password: old_password:
type: string type: string
type: object type: object
v1.authInfo:
properties:
local:
$ref: '#/definitions/v1.localAuthInfo'
openid_connect:
$ref: '#/definitions/v1.openIDAuthInfo'
type: object
v1.legalInfo: v1.legalInfo:
properties: properties:
imprint_url: imprint_url:
@ -923,8 +948,26 @@ definitions:
privacy_policy_url: privacy_policy_url:
type: string type: string
type: object type: object
v1.localAuthInfo:
properties:
enabled:
type: boolean
type: object
v1.openIDAuthInfo:
properties:
enabled:
type: boolean
providers:
items:
$ref: '#/definitions/openid.Provider'
type: array
redirect_url:
type: string
type: object
v1.vikunjaInfos: v1.vikunjaInfos:
properties: properties:
auth:
$ref: '#/definitions/v1.authInfo'
available_migrators: available_migrators:
items: items:
type: string type: string
@ -1021,6 +1064,39 @@ paths:
summary: User Avatar summary: User Avatar
tags: tags:
- user - user
/auth/openid/{provider}/callback:
post:
consumes:
- application/json
description: After a redirect from the OpenID Connect provider to the frontend has been made with the authentication `code`, this endpoint can be used to obtain a jwt token for that user and thus log them in.
parameters:
- description: The openid callback
in: body
name: callback
required: true
schema:
$ref: '#/definitions/openid.Callback'
- description: The OpenID Connect provider key as returned by the /info endpoint
in: path
name: provider
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/auth.Token'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Authenticate a user with OpenID Connect
tags:
- auth
/backgrounds/unsplash/image/{image}: /backgrounds/unsplash/image/{image}:
get: get:
description: Get an unsplash image. **Returns json on error.** description: Get an unsplash image. **Returns json on error.**
@ -2558,7 +2634,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/v1.Token' $ref: '#/definitions/auth.Token'
"400": "400":
description: Invalid user password model. description: Invalid user password model.
schema: schema:
@ -3354,7 +3430,7 @@ paths:
"200": "200":
description: The valid jwt auth token. description: The valid jwt auth token.
schema: schema:
$ref: '#/definitions/v1.Token' $ref: '#/definitions/auth.Token'
"400": "400":
description: Invalid link share object provided. description: Invalid link share object provided.
schema: schema:
@ -4997,7 +5073,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/v1.Token' $ref: '#/definitions/auth.Token'
"400": "400":
description: Only user token are available for renew. description: Only user token are available for renew.
schema: schema:

View file

@ -23,10 +23,6 @@ import (
"reflect" "reflect"
"time" "time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web" "code.vikunja.io/web"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -49,7 +45,7 @@ type User struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"` ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
// The username of the user. Is always unique. // The username of the user. Is always unique.
Username string `xorm:"varchar(250) not null unique" json:"username" valid:"length(1|250)" minLength:"1" maxLength:"250"` Username string `xorm:"varchar(250) not null unique" json:"username" valid:"length(1|250)" minLength:"1" maxLength:"250"`
Password string `xorm:"varchar(250) not null" json:"-"` Password string `xorm:"varchar(250) null" json:"-"`
// The user's email address. // The user's email address.
Email string `xorm:"varchar(250) null" json:"email,omitempty" valid:"email,length(0|250)" maxLength:"250"` Email string `xorm:"varchar(250) null" json:"email,omitempty" valid:"email,length(0|250)" maxLength:"250"`
IsActive bool `xorm:"null" json:"-"` IsActive bool `xorm:"null" json:"-"`
@ -60,6 +56,10 @@ type User struct {
AvatarProvider string `xorm:"varchar(255) null" json:"-"` AvatarProvider string `xorm:"varchar(255) null" json:"-"`
AvatarFileID int64 `xorn:"null" json:"-"` AvatarFileID int64 `xorn:"null" json:"-"`
// Issuer and Subject contain the issuer and subject from the source the user authenticated with.
Issuer string `xorm:"text null" json:"-"`
Subject string `xorm:"text null" json:"-"`
// A timestamp when this task was created. You cannot change this value. // A timestamp when this task was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"` Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this task was last updated. You cannot change this value. // A timestamp when this task was last updated. You cannot change this value.
@ -222,97 +222,6 @@ func GetUserFromClaims(claims jwt.MapClaims) (user *User, err error) {
return return
} }
// CreateUser creates a new user and inserts it into the database
func CreateUser(user *User) (newUser *User, err error) {
newUser = user
// Check if we have all needed informations
if newUser.Password == "" || newUser.Username == "" || newUser.Email == "" {
return &User{}, ErrNoUsernamePassword{}
}
// Check if the user already existst with that username
exists := true
_, err = GetUserByUsername(newUser.Username)
if err != nil {
if IsErrUserDoesNotExist(err) {
exists = false
} else {
return &User{}, err
}
}
if exists {
return &User{}, ErrUsernameExists{newUser.ID, newUser.Username}
}
// Check if the user already existst with that email
exists = true
_, err = GetUser(&User{Email: newUser.Email})
if err != nil {
if IsErrUserDoesNotExist(err) {
exists = false
} else {
return &User{}, err
}
}
if exists {
return &User{}, ErrUserEmailExists{newUser.ID, newUser.Email}
}
// Hash the password
newUser.Password, err = hashPassword(user.Password)
if err != nil {
return &User{}, err
}
newUser.IsActive = true
if config.MailerEnabled.GetBool() {
// The new user should not be activated until it confirms his mail address
newUser.IsActive = false
// Generate a confirm token
newUser.EmailConfirmToken = utils.MakeRandomString(60)
}
newUser.AvatarProvider = "initials"
// Insert it
_, err = x.Insert(newUser)
if err != nil {
return &User{}, err
}
// Update the metrics
metrics.UpdateCount(1, metrics.ActiveUsersKey)
// Get the full new User
newUserOut, err := GetUser(newUser)
if err != nil {
return &User{}, err
}
// Dont send a mail if we're testing
if !config.MailerEnabled.GetBool() {
return newUserOut, err
}
// Send the user a mail with a link to confirm the mail
data := map[string]interface{}{
"User": newUserOut,
"IsNew": true,
}
mail.SendMailWithTemplate(user.Email, newUserOut.Username+" + Vikunja = <3", "confirm-email", data)
return newUserOut, err
}
// HashPassword hashes a password
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 11)
return string(bytes), err
}
// UpdateUser updates a user // UpdateUser updates a user
func UpdateUser(user *User) (updatedUser *User, err error) { func UpdateUser(user *User) (updatedUser *User, err error) {
@ -340,7 +249,11 @@ func UpdateUser(user *User) (updatedUser *User, err error) {
if user.Email == "" { if user.Email == "" {
user.Email = theUser.Email user.Email = theUser.Email
} else { } else {
uu, err := getUser(&User{Email: user.Email}, true) uu, err := getUser(&User{
Email: user.Email,
Issuer: user.Issuer,
Subject: user.Subject,
}, true)
if err != nil && !IsErrUserDoesNotExist(err) { if err != nil && !IsErrUserDoesNotExist(err) {
return nil, err return nil, err
} }

157
pkg/user/user_create.go Normal file
View file

@ -0,0 +1,157 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package user
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/utils"
"golang.org/x/crypto/bcrypt"
)
const issuerLocal = `local`
// CreateUser creates a new user and inserts it into the database
func CreateUser(user *User) (newUser *User, err error) {
if user.Issuer == "" {
user.Issuer = issuerLocal
}
// Check if we have all needed information
err = checkIfUserIsValid(user)
if err != nil {
return nil, err
}
// Check if the user already exists with that username
err = checkIfUserExists(user)
if err != nil {
return nil, err
}
if user.Issuer == issuerLocal {
// Hash the password
user.Password, err = hashPassword(user.Password)
if err != nil {
return nil, err
}
}
user.IsActive = true
if config.MailerEnabled.GetBool() && user.Issuer == issuerLocal {
// The new user should not be activated until it confirms his mail address
user.IsActive = false
// Generate a confirm token
user.EmailConfirmToken = utils.MakeRandomString(60)
}
user.AvatarProvider = "initials"
// Insert it
_, err = x.Insert(user)
if err != nil {
return nil, err
}
// Update the metrics
metrics.UpdateCount(1, metrics.ActiveUsersKey)
// Get the full new User
newUserOut, err := GetUserByID(user.ID)
if err != nil {
return nil, err
}
sendConfirmEmail(user)
return newUserOut, err
}
// HashPassword hashes a password
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 11)
return string(bytes), err
}
func checkIfUserIsValid(user *User) error {
if user.Email == "" ||
(user.Issuer != issuerLocal && user.Subject == "") ||
(user.Issuer == issuerLocal && (user.Password == "" ||
user.Username == "")) {
return ErrNoUsernamePassword{}
}
return nil
}
func checkIfUserExists(user *User) (err error) {
exists := true
_, err = GetUserByUsername(user.Username)
if err != nil {
if IsErrUserDoesNotExist(err) {
exists = false
} else {
return err
}
}
if exists {
return ErrUsernameExists{user.ID, user.Username}
}
// Check if the user already existst with that email
exists = true
userToCheck := &User{
Email: user.Email,
Issuer: user.Issuer,
Subject: user.Subject,
}
if user.Issuer != issuerLocal {
userToCheck.Email = ""
}
_, err = GetUser(userToCheck)
if err != nil {
if IsErrUserDoesNotExist(err) {
exists = false
} else {
return err
}
}
if exists && user.Issuer == issuerLocal {
return ErrUserEmailExists{user.ID, user.Email}
}
return nil
}
func sendConfirmEmail(user *User) {
// Dont send a mail if no mailer is configured
if !config.MailerEnabled.GetBool() {
return
}
// Send the user a mail with a link to confirm the mail
data := map[string]interface{}{
"User": user,
"IsNew": true,
}
mail.SendMailWithTemplate(user.Email, user.Username+" + Vikunja = <3", "confirm-email", data)
}

View file

@ -88,6 +88,26 @@ func TestCreateUser(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrNoUsernamePassword(err)) assert.True(t, IsErrNoUsernamePassword(err))
}) })
t.Run("same email but different issuer", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
_, err := CreateUser(&User{
Username: "somenewuser",
Email: "user1@example.com",
Issuer: "https://some.site",
Subject: "12345",
})
assert.NoError(t, err)
})
t.Run("same subject but different issuer", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
_, err := CreateUser(&User{
Username: "somenewuser",
Email: "somenewuser@example.com",
Issuer: "https://some.site",
Subject: "12345",
})
assert.NoError(t, err)
})
} }
func TestGetUser(t *testing.T) { func TestGetUser(t *testing.T) {
@ -256,7 +276,7 @@ func TestListUsers(t *testing.T) {
db.LoadAndAssertFixtures(t) db.LoadAndAssertFixtures(t)
all, err := ListUsers("") all, err := ListUsers("")
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, all, 13) assert.Len(t, all, 14)
}) })
} }