// 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 . package openid import ( "code.vikunja.io/api/pkg/modules/auth" "context" "encoding/json" petname "github.com/dustinkirkland/golang-petname" "math/rand" "net/http" "regexp" "strconv" "strings" "time" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" "github.com/coreos/go-oidc" "github.com/labstack/echo/v4" "golang.org/x/oauth2" ) type Callback struct { Code string `query:"code" json:"code"` Scope string `query:"scop" json:"scope"` State string `query:"state" json:"state"` } 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()) } func getKeyFromName(name string) string { reg, _ := regexp.Compile("[^a-z0-9]+") return reg.ReplaceAllString(strings.ToLower(name), "") } func GetAllProviders() (providers []*Provider, err error) { 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) } return } 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 } func GetProvider(key string) (*Provider, error) { rawProvider := config.AuthOpenIDProviders.Get().([]interface{}) for _, p := range rawProvider { pi := p.(map[interface{}]interface{}) k := getKeyFromName(pi["name"].(string)) if k == key { return getProviderFromMap(pi) } } return nil, nil } 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.GetUser(&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, } 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) } } else { // 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 }