Authentication with OpenID Connect providers #713
|
@ -215,3 +215,32 @@ legal:
|
|||
keyvalue:
|
||||
# The type of the storage backend. Can be either "memory" or "redis". If "redis" is chosen it needs to be configured seperately.
|
||||
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:
|
||||
|
|
|
@ -563,3 +563,27 @@ The type of the storage backend. Can be either "memory" or "redis". If "redis" i
|
|||
|
||||
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
5
go.mod
|
@ -26,10 +26,12 @@ require (
|
|||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
||||
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/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
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/gabriel-vasile/mimetype v1.1.2
|
||||
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/pelletier/go-toml v1.8.0 // indirect
|
||||
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/samedi/caldav-go v3.0.0+incompatible
|
||||
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/lint v0.0.0-20200302205851-738671d3881b
|
||||
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/tools v0.0.0-20201017001424-6003fad69a88 // 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/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
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
|
||||
honnef.co/go/tools v0.0.1-2020.1.5
|
||||
src.techknowlogick.com/xgo v1.1.1-0.20200811225412-bff6512e7c9c
|
||||
|
|
8
go.sum
8
go.sum
|
@ -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.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-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.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
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/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/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-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=
|
||||
|
@ -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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
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/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/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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
|
||||
"code.vikunja.io/api/pkg/initialize"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -157,11 +158,16 @@ var userCreateCmd = &cobra.Command{
|
|||
Email: userFlagEmail,
|
||||
Password: getPasswordFromFlagOrInput(),
|
||||
}
|
||||
_, err := user.CreateUser(u)
|
||||
newUser, err := user.CreateUser(u)
|
||||
if err != nil {
|
||||
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")
|
||||
},
|
||||
}
|
||||
|
|
|
@ -52,6 +52,11 @@ const (
|
|||
ServiceEnableTotp Key = `service.enabletotp`
|
||||
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`
|
||||
LegalPrivacyURL Key = `legal.privacyurl`
|
||||
|
||||
|
@ -158,6 +163,11 @@ func (k Key) GetStringSlice() []string {
|
|||
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
|
||||
|
||||
// GetTimeZone returns the time zone configured for vikunja
|
||||
|
@ -216,6 +226,10 @@ func InitDefaultConfig() {
|
|||
ServiceEnableTaskComments.setDefault(true)
|
||||
ServiceEnableTotp.setDefault(true)
|
||||
|
||||
// Auth
|
||||
AuthLocalEnabled.setDefault(true)
|
||||
AuthOpenIDEnabled.setDefault(false)
|
||||
|
||||
// Database
|
||||
DatabaseType.setDefault("sqlite")
|
||||
DatabaseHost.setDefault("localhost")
|
||||
|
@ -322,6 +336,10 @@ func InitConfig() {
|
|||
RateLimitStore.Set(KeyvalueType.GetString())
|
||||
}
|
||||
|
||||
if AuthOpenIDRedirectURL.GetString() == "" {
|
||||
AuthOpenIDRedirectURL.Set(ServiceFrontendurl.GetString() + "auth/openid/")
|
||||
}
|
||||
|
||||
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user1@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -11,6 +12,7 @@
|
|||
username: 'user2'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user2@example.com'
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -19,6 +21,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user3@example.com'
|
||||
password_reset_token: passwordresettesttoken
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -27,6 +30,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user4@example.com'
|
||||
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -36,6 +40,7 @@
|
|||
email: 'user5@example.com'
|
||||
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
|
||||
is_active: false
|
||||
issuer: local
|
||||
updated: 2018-12-02 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
|
||||
|
@ -44,6 +49,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user6@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 7
|
||||
|
@ -51,6 +57,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user7@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
|
@ -58,6 +65,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user8@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 9
|
||||
|
@ -65,6 +73,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user9@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 10
|
||||
|
@ -72,6 +81,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user10@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 11
|
||||
|
@ -79,6 +89,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user11@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 12
|
||||
|
@ -86,6 +97,7 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user12@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 13
|
||||
|
@ -93,5 +105,15 @@
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user14@example.com'
|
||||
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
|
||||
created: 2018-12-01 15:13:12
|
||||
|
|
|
@ -28,8 +28,8 @@ import (
|
|||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/routes"
|
||||
v1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"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) {
|
||||
// Get the token as a string
|
||||
token, err := v1.NewUserJWTAuthtoken(user)
|
||||
token, err := auth.NewUserJWTAuthtoken(user)
|
||||
assert.NoError(t, err)
|
||||
// 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) {
|
||||
|
@ -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) {
|
||||
// Get the token as a string
|
||||
token, err := v1.NewLinkShareJWTAuthtoken(share)
|
||||
token, err := auth.NewLinkShareJWTAuthtoken(share)
|
||||
assert.NoError(t, err)
|
||||
// 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) {
|
||||
|
|
|
@ -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
|
||||
},
|
||||
})
|
||||
}
|
|
@ -56,6 +56,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -52,6 +52,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -99,6 +100,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -159,6 +161,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -217,6 +220,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -176,6 +176,7 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -186,6 +187,7 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -445,6 +445,16 @@ func (n *Namespace) Create(a web.Auth) (err error) {
|
|||
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
|
||||
// @Summary Deletes a namespace
|
||||
// @Description Delets a namespace
|
||||
|
|
|
@ -175,6 +175,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -185,6 +186,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -35,6 +35,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -42,6 +43,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -49,6 +51,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
IsActive: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
|
|
|
@ -31,6 +31,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -38,6 +39,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -46,6 +48,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
PasswordResetToken: "passwordresettesttoken",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -55,6 +58,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -64,6 +68,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -72,6 +77,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -80,6 +86,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user7",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -88,6 +95,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user8",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -96,6 +104,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user9",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -104,6 +113,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user10",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -112,6 +122,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user11",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -120,6 +131,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user12",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -128,6 +140,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Username: "user13",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
// 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 v1
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -35,6 +35,21 @@ const (
|
|||
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.
|
||||
func NewUserJWTAuthtoken(user *user.User) (token string, err error) {
|
||||
t := jwt.New(jwt.SigningMethodHS256)
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -25,9 +25,9 @@ import (
|
|||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"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/unsplash"
|
||||
v1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"code.vikunja.io/web"
|
||||
"code.vikunja.io/web/handler"
|
||||
"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
|
||||
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 {
|
||||
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]
|
||||
func GetListBackground(c echo.Context) error {
|
||||
|
||||
auth, err := v1.GetAuthFromClaims(c)
|
||||
auth, err := auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error())
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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/wunderlist"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
|
@ -39,6 +40,22 @@ type vikunjaInfos struct {
|
|||
TotpEnabled bool `json:"totp_enabled"`
|
||||
Legal legalInfo `json:"legal"`
|
||||
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 {
|
||||
|
@ -68,8 +85,24 @@ func Info(c echo.Context) error {
|
|||
ImprintURL: config.LegalImprintURL.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
|
||||
if config.MigrationWunderlistEnable.GetBool() {
|
||||
m := &wunderlist.Migration{}
|
||||
|
|
|
@ -20,13 +20,14 @@ import (
|
|||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// LinkShareToken represents a link share auth token with extra infos about the actual link share
|
||||
type LinkShareToken struct {
|
||||
Token
|
||||
auth.Token
|
||||
*models.LinkSharing
|
||||
ListID int64 `json:"list_id"`
|
||||
}
|
||||
|
@ -38,7 +39,7 @@ type LinkShareToken struct {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @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 500 {object} models.Message "Internal error"
|
||||
// @Router /shares/{share}/auth [post]
|
||||
|
@ -49,13 +50,13 @@ func AuthenticateLinkShare(c echo.Context) error {
|
|||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
t, err := NewLinkShareJWTAuthtoken(share)
|
||||
t, err := auth.NewLinkShareJWTAuthtoken(share)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, LinkShareToken{
|
||||
Token: Token{Token: t},
|
||||
Token: auth.Token{Token: t},
|
||||
LinkSharing: share,
|
||||
ListID: share.ListID,
|
||||
})
|
||||
|
|
|
@ -20,17 +20,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Token represents an authentification token
|
||||
type Token struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// Login is the login handler
|
||||
// @Summary Login
|
||||
// @Description Logs a user in. Returns a JWT-Token to authenticate further requests.
|
||||
|
@ -38,7 +34,7 @@ type Token struct {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @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 412 {object} models.Message "Invalid totp passcode."
|
||||
// @Failure 403 {object} models.Message "Invalid username or password."
|
||||
|
@ -71,12 +67,7 @@ func Login(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Create token
|
||||
t, err := NewUserJWTAuthtoken(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, Token{Token: t})
|
||||
return auth.NewUserAuthTokenResponse(user, c)
|
||||
}
|
||||
|
||||
// RenewToken gives a new token to every user with a valid token
|
||||
|
@ -86,7 +77,7 @@ func Login(c echo.Context) error {
|
|||
// @tags user
|
||||
// @Accept 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."
|
||||
// @Router /user/token [post]
|
||||
func RenewToken(c echo.Context) (err error) {
|
||||
|
@ -94,18 +85,18 @@ func RenewToken(c echo.Context) (err error) {
|
|||
jwtinf := c.Get("user").(*jwt.Token)
|
||||
claims := jwtinf.Claims.(jwt.MapClaims)
|
||||
typ := int(claims["type"].(float64))
|
||||
if typ == AuthTypeLinkShare {
|
||||
if typ == auth.AuthTypeLinkShare {
|
||||
share := &models.LinkSharing{}
|
||||
share.ID = int64(claims["id"].(float64))
|
||||
err := share.ReadOne()
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
t, err := NewLinkShareJWTAuthtoken(share)
|
||||
t, err := auth.NewLinkShareJWTAuthtoken(share)
|
||||
if err != nil {
|
||||
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)
|
||||
|
@ -114,10 +105,5 @@ func RenewToken(c echo.Context) (err error) {
|
|||
}
|
||||
|
||||
// Create token
|
||||
t, err := NewUserJWTAuthtoken(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, Token{Token: t})
|
||||
return auth.NewUserAuthTokenResponse(user, c)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
auth2 "code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
@ -46,7 +47,7 @@ func UploadTaskAttachment(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Rights check
|
||||
auth, err := GetAuthFromClaims(c)
|
||||
auth, err := auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
@ -116,7 +117,7 @@ func GetTaskAttachment(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Rights check
|
||||
auth, err := GetAuthFromClaims(c)
|
||||
auth, err := auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
auth2 "code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -74,7 +75,7 @@ func ListUsersForList(c echo.Context) error {
|
|||
}
|
||||
|
||||
list := models.List{ID: listID}
|
||||
auth, err := GetAuthFromClaims(c)
|
||||
auth, err := auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
|
|
@ -57,8 +57,7 @@ func RegisterUser(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Add its namespace
|
||||
newN := &models.Namespace{Title: newUser.Username, Description: newUser.Username + "'s namespace.", Owner: newUser}
|
||||
err = newN.Create(newUser)
|
||||
err = models.CreateNewNamespaceForUser(newUser)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"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"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
@ -95,7 +95,7 @@ func setupMetricsMiddleware(a *echo.Group) {
|
|||
|
||||
// updateActiveUsersFromContext updates the currently active users in redis
|
||||
func updateActiveUsersFromContext(c echo.Context) (err error) {
|
||||
auth, err := v1.GetAuthFromClaims(c)
|
||||
auth, err := auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ import (
|
|||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
auth2 "code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/red"
|
||||
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/ulule/limiter/v3"
|
||||
"github.com/ulule/limiter/v3/drivers/store/memory"
|
||||
|
@ -41,7 +41,7 @@ func RateLimit(rateLimiter *limiter.Limiter, rateLimitKind string) echo.Middlewa
|
|||
case "ip":
|
||||
rateLimitKey = c.RealIP()
|
||||
case "user":
|
||||
auth, err := apiv1.GetAuthFromClaims(c)
|
||||
auth, err := auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting auth from jwt claims: %v", err)
|
||||
}
|
||||
|
|
|
@ -53,6 +53,8 @@ import (
|
|||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"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"
|
||||
backgroundHandler "code.vikunja.io/api/pkg/modules/background/handler"
|
||||
"code.vikunja.io/api/pkg/modules/background/unsplash"
|
||||
|
@ -165,7 +167,7 @@ func NewEcho() *echo.Echo {
|
|||
|
||||
// Handler config
|
||||
handler.SetAuthProvider(&web.Auths{
|
||||
AuthObject: apiv1.GetAuthFromClaims,
|
||||
AuthObject: auth.GetAuthFromClaims,
|
||||
})
|
||||
handler.SetLoggingProvider(log.GetLogger())
|
||||
handler.SetMaxItemsPerPage(config.ServiceMaxItemsPerPage.GetInt())
|
||||
|
@ -220,12 +222,18 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
// Prometheus endpoint
|
||||
setupMetrics(n)
|
||||
|
||||
// User stuff
|
||||
n.POST("/login", apiv1.Login)
|
||||
n.POST("/register", apiv1.RegisterUser)
|
||||
n.POST("/user/password/token", apiv1.UserRequestResetPasswordToken)
|
||||
n.POST("/user/password/reset", apiv1.UserResetPassword)
|
||||
n.POST("/user/confirm", apiv1.UserConfirmEmail)
|
||||
if config.AuthLocalEnabled.GetBool() {
|
||||
// User stuff
|
||||
n.POST("/login", apiv1.Login)
|
||||
n.POST("/register", apiv1.RegisterUser)
|
||||
n.POST("/user/password/token", apiv1.UserRequestResetPasswordToken)
|
||||
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
|
||||
n.GET("/info", apiv1.Info)
|
||||
|
|
|
@ -32,6 +32,58 @@ var doc = `{
|
|||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"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}": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2426,7 +2478,7 @@ var doc = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.Token"
|
||||
"$ref": "#/definitions/auth.Token"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -3671,7 +3723,7 @@ var doc = `{
|
|||
"200": {
|
||||
"description": "The valid jwt auth token.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.Token"
|
||||
"$ref": "#/definitions/auth.Token"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -6240,7 +6292,7 @@ var doc = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.Token"
|
||||
"$ref": "#/definitions/auth.Token"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -6352,6 +6404,14 @@ var doc = `{
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"auth.Token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"background.Image": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7577,14 +7665,6 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.Token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserAvatarProvider": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auth": {
|
||||
"$ref": "#/definitions/v1.authInfo"
|
||||
},
|
||||
"available_migrators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -15,6 +15,58 @@
|
|||
},
|
||||
"basePath": "/api/v1",
|
||||
"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}": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2409,7 +2461,7 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.Token"
|
||||
"$ref": "#/definitions/auth.Token"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -3654,7 +3706,7 @@
|
|||
"200": {
|
||||
"description": "The valid jwt auth token.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.Token"
|
||||
"$ref": "#/definitions/auth.Token"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -6223,7 +6275,7 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.Token"
|
||||
"$ref": "#/definitions/auth.Token"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -6335,6 +6387,14 @@
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"auth.Token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"background.Image": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7560,14 +7648,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.Token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserAvatarProvider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7587,6 +7667,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.authInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"$ref": "#/definitions/v1.localAuthInfo"
|
||||
},
|
||||
"openid_connect": {
|
||||
"$ref": "#/definitions/v1.openIDAuthInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.legalInfo": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auth": {
|
||||
"$ref": "#/definitions/v1.authInfo"
|
||||
},
|
||||
"available_migrators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
basePath: /api/v1
|
||||
definitions:
|
||||
auth.Token:
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
background.Image:
|
||||
properties:
|
||||
id:
|
||||
|
@ -795,6 +800,24 @@ definitions:
|
|||
minLength: 1
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
code:
|
||||
|
@ -899,11 +922,6 @@ definitions:
|
|||
minLength: 1
|
||||
type: string
|
||||
type: object
|
||||
v1.Token:
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.UserAvatarProvider:
|
||||
properties:
|
||||
avatar_provider:
|
||||
|
@ -916,6 +934,13 @@ definitions:
|
|||
old_password:
|
||||
type: string
|
||||
type: object
|
||||
v1.authInfo:
|
||||
properties:
|
||||
local:
|
||||
$ref: '#/definitions/v1.localAuthInfo'
|
||||
openid_connect:
|
||||
$ref: '#/definitions/v1.openIDAuthInfo'
|
||||
type: object
|
||||
v1.legalInfo:
|
||||
properties:
|
||||
imprint_url:
|
||||
|
@ -923,8 +948,26 @@ definitions:
|
|||
privacy_policy_url:
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
auth:
|
||||
$ref: '#/definitions/v1.authInfo'
|
||||
available_migrators:
|
||||
items:
|
||||
type: string
|
||||
|
@ -1021,6 +1064,39 @@ paths:
|
|||
summary: User Avatar
|
||||
tags:
|
||||
- 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}:
|
||||
get:
|
||||
description: Get an unsplash image. **Returns json on error.**
|
||||
|
@ -2558,7 +2634,7 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.Token'
|
||||
$ref: '#/definitions/auth.Token'
|
||||
"400":
|
||||
description: Invalid user password model.
|
||||
schema:
|
||||
|
@ -3354,7 +3430,7 @@ paths:
|
|||
"200":
|
||||
description: The valid jwt auth token.
|
||||
schema:
|
||||
$ref: '#/definitions/v1.Token'
|
||||
$ref: '#/definitions/auth.Token'
|
||||
"400":
|
||||
description: Invalid link share object provided.
|
||||
schema:
|
||||
|
@ -4997,7 +5073,7 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.Token'
|
||||
$ref: '#/definitions/auth.Token'
|
||||
"400":
|
||||
description: Only user token are available for renew.
|
||||
schema:
|
||||
|
|
107
pkg/user/user.go
107
pkg/user/user.go
|
@ -23,10 +23,6 @@ import (
|
|||
"reflect"
|
||||
"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"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -49,7 +45,7 @@ type User struct {
|
|||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
// 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"`
|
||||
Password string `xorm:"varchar(250) not null" json:"-"`
|
||||
Password string `xorm:"varchar(250) null" json:"-"`
|
||||
// The user's email address.
|
||||
Email string `xorm:"varchar(250) null" json:"email,omitempty" valid:"email,length(0|250)" maxLength:"250"`
|
||||
IsActive bool `xorm:"null" json:"-"`
|
||||
|
@ -60,6 +56,10 @@ type User struct {
|
|||
AvatarProvider string `xorm:"varchar(255) 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.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
func UpdateUser(user *User) (updatedUser *User, err error) {
|
||||
|
||||
|
@ -340,7 +249,11 @@ func UpdateUser(user *User) (updatedUser *User, err error) {
|
|||
if user.Email == "" {
|
||||
user.Email = theUser.Email
|
||||
} 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) {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -88,6 +88,26 @@ func TestCreateUser(t *testing.T) {
|
|||
assert.Error(t, 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) {
|
||||
|
@ -256,7 +276,7 @@ func TestListUsers(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
all, err := ListUsers("")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 13)
|
||||
assert.Len(t, all, 14)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue