vikunja/pkg/modules/auth/openid/openid.go

200 lines
5.2 KiB
Go

// 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 (
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
"context"
"encoding/json"
"net/http"
"regexp"
"strings"
"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 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),
ClientID: pi["clientid"].(string),
ClientSecret: pi["clientsecret"].(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 apiv1.NewUserAuthTokenResponse(u, c)
}
func getOrCreateUser() (u *user.User, err error) {
return
}