2020-11-21 16:38:58 +00:00
// Vikunja is a to-do list application to facilitate your life.
2021-02-02 19:19:13 +00:00
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
2020-11-21 16:38:58 +00:00
//
// This program is free software: you can redistribute it and/or modify
2020-12-23 15:41:52 +00:00
// it under the terms of the GNU Affero General Public Licensee as published by
2020-11-21 16:38:58 +00:00
// 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
2020-12-23 15:41:52 +00:00
// GNU Affero General Public Licensee for more details.
2020-11-21 16:38:58 +00:00
//
2020-12-23 15:41:52 +00:00
// You should have received a copy of the GNU Affero General Public Licensee
2020-11-21 16:38:58 +00:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package openid
import (
"context"
"encoding/json"
2022-03-27 14:55:37 +00:00
"errors"
2020-11-21 16:38:58 +00:00
"math/rand"
"net/http"
2023-02-13 18:52:18 +00:00
"strconv"
2020-11-21 16:38:58 +00:00
"time"
2021-05-16 11:23:10 +00:00
"code.vikunja.io/web/handler"
2020-12-23 15:32:28 +00:00
"code.vikunja.io/api/pkg/db"
2020-11-21 16:38:58 +00:00
"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"
2023-02-13 18:52:18 +00:00
"code.vikunja.io/api/pkg/utils"
2021-06-14 20:56:29 +00:00
"github.com/coreos/go-oidc/v3/oidc"
2020-11-21 16:38:58 +00:00
petname "github.com/dustinkirkland/golang-petname"
"github.com/labstack/echo/v4"
"golang.org/x/oauth2"
2023-02-13 18:52:18 +00:00
"xorm.io/xorm"
2020-11-21 16:38:58 +00:00
)
// 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 {
2021-06-09 21:00:42 +00:00
Name string ` json:"name" `
Key string ` json:"key" `
OriginalAuthURL string ` json:"-" `
AuthURL string ` json:"auth_url" `
2022-12-18 18:26:28 +00:00
LogoutURL string ` json:"logout_url" `
2021-06-09 21:00:42 +00:00
ClientID string ` json:"client_id" `
2022-10-12 13:11:45 +00:00
Scope string ` json:"scope" `
2021-06-09 21:00:42 +00:00
ClientSecret string ` json:"-" `
openIDProvider * oidc . Provider
Oauth2Config * oauth2 . Config ` json:"-" `
2020-11-21 16:38:58 +00:00
}
type claims struct {
2023-02-23 15:17:59 +00:00
Email string ` json:"email" `
Name string ` json:"name" `
PreferredUsername string ` json:"preferred_username" `
Nickname string ` json:"nickname" `
VikunjaGroups [ ] map [ string ] interface { } ` json:"vikunja_groups" `
2020-11-21 16:38:58 +00:00
}
func init ( ) {
rand . Seed ( time . Now ( ) . UTC ( ) . UnixNano ( ) )
}
2021-05-28 08:52:32 +00:00
func ( p * Provider ) setOicdProvider ( ) ( err error ) {
2021-06-09 21:00:42 +00:00
p . openIDProvider , err = oidc . NewProvider ( context . Background ( ) , p . OriginalAuthURL )
2021-05-28 08:52:32 +00:00
return err
}
2020-11-21 16:38:58 +00:00
// 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 )
2021-05-16 11:23:10 +00:00
return handler . HandleHTTPError ( err , c )
2020-11-21 16:38:58 +00:00
}
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 {
2022-03-27 14:55:37 +00:00
var rerr * oauth2 . RetrieveError
if errors . As ( err , & rerr ) {
2020-11-21 16:38:58 +00:00
log . Error ( err )
details := make ( map [ string ] interface { } )
if err := json . Unmarshal ( rerr . Body , & details ) ; err != nil {
2022-03-27 14:55:37 +00:00
log . Errorf ( "Error unmarshalling token for provider %s: %v" , provider . Name , err )
2021-05-16 11:23:10 +00:00
return handler . HandleHTTPError ( err , c )
2020-11-21 16:38:58 +00:00
}
return c . JSON ( http . StatusBadRequest , map [ string ] interface { } {
"message" : "Could not authenticate against third party." ,
"details" : details ,
} )
}
2021-05-16 11:23:10 +00:00
return handler . HandleHTTPError ( err , c )
2020-11-21 16:38:58 +00:00
}
// 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" } )
}
2021-05-28 08:52:32 +00:00
verifier := provider . openIDProvider . Verifier ( & oidc . Config { ClientID : provider . ClientID } )
2020-11-21 16:38:58 +00:00
// Parse and verify ID Token payload.
idToken , err := verifier . Verify ( context . Background ( ) , rawIDToken )
if err != nil {
2021-05-16 11:23:10 +00:00
log . Errorf ( "Error verifying token for provider %s: %v" , provider . Name , err )
return handler . HandleHTTPError ( err , c )
2020-11-21 16:38:58 +00:00
}
// Extract custom claims
cl := & claims { }
2023-01-27 12:49:19 +00:00
2020-11-21 16:38:58 +00:00
err = idToken . Claims ( cl )
if err != nil {
2021-05-16 11:23:10 +00:00
log . Errorf ( "Error getting token claims for provider %s: %v" , provider . Name , err )
return handler . HandleHTTPError ( err , c )
}
2021-05-19 12:45:24 +00:00
if cl . Email == "" || cl . Name == "" || cl . PreferredUsername == "" {
2021-05-28 08:52:32 +00:00
info , err := provider . openIDProvider . UserInfo ( context . Background ( ) , provider . Oauth2Config . TokenSource ( context . Background ( ) , oauth2Token ) )
2021-05-19 12:45:24 +00:00
if err != nil {
log . Errorf ( "Error getting userinfo for provider %s: %v" , provider . Name , err )
return handler . HandleHTTPError ( err , c )
}
cl2 := & claims { }
err = info . Claims ( cl2 )
if err != nil {
log . Errorf ( "Error parsing userinfo claims for provider %s: %v" , provider . Name , err )
return handler . HandleHTTPError ( err , c )
}
if cl . Email == "" {
cl . Email = cl2 . Email
}
if cl . Name == "" {
cl . Name = cl2 . Name
}
if cl . PreferredUsername == "" {
cl . PreferredUsername = cl2 . PreferredUsername
}
if cl . PreferredUsername == "" && cl2 . Nickname != "" {
cl . PreferredUsername = cl2 . Nickname
}
if cl . Email == "" {
log . Errorf ( "Claim does not contain an email address for provider %s" , provider . Name )
return handler . HandleHTTPError ( & user . ErrNoOpenIDEmailProvided { } , c )
}
2020-11-21 16:38:58 +00:00
}
2020-12-23 15:32:28 +00:00
s := db . NewSession ( )
defer s . Close ( )
2020-11-21 16:38:58 +00:00
// Check if we have seen this user before
2020-12-23 15:32:28 +00:00
u , err := getOrCreateUser ( s , cl , idToken . Issuer , idToken . Subject )
if err != nil {
_ = s . Rollback ( )
2021-05-16 11:23:10 +00:00
log . Errorf ( "Error creating new user for provider %s: %v" , provider . Name , err )
return handler . HandleHTTPError ( err , c )
2020-12-23 15:32:28 +00:00
}
2023-01-27 12:49:19 +00:00
// does the oidc token contain well formed "vikunja_groups" through vikunja_scope
2023-02-13 18:52:18 +00:00
teamData , errs := getTeamDataFromToken ( cl . VikunjaGroups , provider )
2023-02-13 19:43:24 +00:00
for _ , err := range errs {
log . Errorf ( "Error creating teams for user and vikunja groups %s: %v" , cl . VikunjaGroups , err )
2023-01-27 12:49:19 +00:00
}
2023-01-27 16:15:50 +00:00
2023-02-01 15:35:12 +00:00
//find old teams for user through oidc
2023-02-13 18:52:18 +00:00
oldOidcTeams , err := models . FindAllOidcTeamIDsForUser ( s , u . ID )
if err != nil {
2023-02-13 19:43:24 +00:00
log . Errorf ( "No Oidc Teams found for user %v" , err )
}
oidcTeams , err := AssignOrCreateUserToTeams ( s , u , teamData )
if err != nil {
log . Errorf ( "Could not proceed with group routine %v" , err )
}
2023-02-23 14:23:32 +00:00
errs = SignOutFromTeamsByID ( s , u , utils . NotIn ( oldOidcTeams , oidcTeams ) )
log . Errorf ( "%v" , errs )
2023-02-13 19:43:24 +00:00
for _ , err := range errs {
log . Errorf ( "Found Error while signing out from teams %v" , err )
}
err = s . Commit ( )
if err != nil {
_ = s . Rollback ( )
log . Errorf ( "Error creating new Team for provider %s: %v" , provider . Name , err )
return handler . HandleHTTPError ( err , c )
2023-02-13 18:52:18 +00:00
}
2023-02-13 19:43:24 +00:00
// Create token
return auth . NewUserAuthTokenResponse ( u , c , false )
}
func AssignOrCreateUserToTeams ( s * xorm . Session , u * user . User , teamData [ ] models . TeamData ) ( oidcTeams [ ] int64 , err error ) {
2023-01-27 12:49:19 +00:00
if len ( teamData ) > 0 {
2023-01-27 16:15:50 +00:00
// check if we have seen these teams before.
// find or create Teams and assign user as teammember.
2023-01-27 15:53:14 +00:00
teams , err := GetOrCreateTeamsByOIDCAndNames ( s , teamData , u )
2022-12-07 14:32:58 +00:00
if err != nil {
2023-02-13 19:43:24 +00:00
log . Errorf ( "Error verifying team for %v, got %v. Error: %v" , u . Name , teams , err )
return nil , err
2023-01-27 12:49:19 +00:00
}
for _ , team := range teams {
2023-02-13 18:52:18 +00:00
tm := models . TeamMember { TeamID : team . ID , UserID : u . ID , Username : u . Username }
2023-02-13 19:43:24 +00:00
exists , _ := tm . CheckMembership ( s )
2023-01-27 12:49:19 +00:00
if ! exists {
err = tm . Create ( s , u )
if err != nil {
2023-02-13 18:52:18 +00:00
log . Errorf ( "Could not assign %v to %v. %v" , u . Username , team . Name , err )
2022-10-12 13:11:45 +00:00
}
}
2023-01-27 16:15:50 +00:00
oidcTeams = append ( oidcTeams , team . ID )
2022-10-12 13:11:45 +00:00
}
}
2023-02-13 19:43:24 +00:00
return oidcTeams , err
2020-11-21 16:38:58 +00:00
2023-02-13 19:43:24 +00:00
}
2023-02-23 14:23:32 +00:00
func SignOutFromTeamsByID ( s * xorm . Session , u * user . User , teamIDs [ ] int64 ) ( errs [ ] error ) {
2023-02-13 18:52:18 +00:00
errs = [ ] error { }
2023-01-27 16:15:50 +00:00
for _ , teamID := range teamIDs {
2023-02-13 18:52:18 +00:00
tm := models . TeamMember { TeamID : teamID , UserID : u . ID , Username : u . Username }
2023-02-23 14:23:32 +00:00
exists , err := tm . CheckMembership ( s )
if err != nil {
errs = append ( errs , err )
continue
}
2023-02-13 18:52:18 +00:00
if ! exists {
continue
}
2023-02-23 14:23:32 +00:00
err = tm . Delete ( s , u )
2023-02-13 18:52:18 +00:00
// if you cannot delete the team_member
2023-02-23 14:23:32 +00:00
if err != nil {
errs = append ( errs , err )
continue
2023-01-27 16:15:50 +00:00
}
}
2023-02-13 18:52:18 +00:00
return errs
2023-01-27 16:15:50 +00:00
}
2023-02-23 15:17:59 +00:00
func getTeamDataFromToken ( groups [ ] map [ string ] interface { } , provider * Provider ) ( teamData [ ] models . TeamData , errs [ ] error ) {
2023-02-13 16:26:22 +00:00
teamData = [ ] models . TeamData { }
2023-02-13 18:52:18 +00:00
errs = [ ] error { }
2023-02-23 15:17:59 +00:00
for _ , team := range groups {
var name string
var description string
var oidcID string
if team [ "name" ] != nil {
name = team [ "name" ] . ( string )
}
if team [ "description" ] != nil {
description = team [ "description" ] . ( string )
}
if team [ "oidcID" ] != nil {
switch t := team [ "oidcID" ] . ( type ) {
case int64 :
oidcID = strconv . FormatInt ( team [ "oidcID" ] . ( int64 ) , 10 )
case string :
oidcID = string ( team [ "oidcID" ] . ( string ) )
case float64 :
oidcID = strconv . FormatFloat ( team [ "oidcID" ] . ( float64 ) , 'f' , - 1 , 64 )
default :
log . Errorf ( "No oidcID assigned for %v or type %v not supported" , team , t )
2022-10-12 13:11:45 +00:00
}
2023-01-27 12:49:19 +00:00
}
2023-02-23 15:17:59 +00:00
if name == "" || oidcID == "" {
log . Errorf ( "Claim of your custom scope does not hold name or oidcID for automatic group assignment through oidc provider. Please check %s" , provider . Name )
errs = append ( errs , & user . ErrOpenIDCustomScopeMalformed { } )
continue
}
teamData = append ( teamData , models . TeamData { TeamName : name , OidcID : oidcID , Description : description } )
2023-01-27 12:49:19 +00:00
}
2023-02-13 18:52:18 +00:00
return teamData , errs
2023-01-27 12:49:19 +00:00
}
2023-02-13 16:26:22 +00:00
func CreateTeamWithData ( s * xorm . Session , teamData models . TeamData , u * user . User ) ( team * models . Team , err error ) {
2023-01-27 12:49:19 +00:00
tea := & models . Team {
Name : teamData . TeamName ,
Description : teamData . Description ,
OidcID : teamData . OidcID ,
}
err = tea . Create ( s , u )
return tea , err
}
// this functions creates an array of existing teams that was generated from the oidc data.
2023-02-23 15:17:59 +00:00
func GetOrCreateTeamsByOIDCAndNames ( s * xorm . Session , teamData [ ] models . TeamData , u * user . User ) ( te [ ] * models . Team , err error ) {
te = [ ] * models . Team { }
2023-01-27 12:49:19 +00:00
// Procedure can only be successful if oidcID is set and converted to string
for _ , oidcTeam := range teamData {
team , err := models . GetTeamByOidcIDAndName ( s , oidcTeam . OidcID , oidcTeam . TeamName )
if err != nil {
log . Debugf ( "Team with oidc_id %v and name %v does not exist. Create Team.. " , oidcTeam . OidcID , oidcTeam . TeamName )
newTeam , err := CreateTeamWithData ( s , oidcTeam , u )
2022-10-12 13:11:45 +00:00
if err != nil {
2023-01-27 12:49:19 +00:00
return te , err
2022-10-12 13:11:45 +00:00
}
2023-02-23 15:17:59 +00:00
te = append ( te , newTeam )
2022-10-12 13:11:45 +00:00
} else {
2023-01-27 12:49:19 +00:00
log . Debugf ( "Team with oidc_id %v and name %v already exists." , team . OidcID , team . Name )
2023-02-23 15:17:59 +00:00
te = append ( te , & team )
2022-10-12 13:11:45 +00:00
}
}
return te , err
}
2020-12-23 15:32:28 +00:00
func getOrCreateUser ( s * xorm . Session , cl * claims , issuer , subject string ) ( u * user . User , err error ) {
2022-10-12 13:11:45 +00:00
2020-11-21 16:38:58 +00:00
// Check if the user exists for that issuer and subject
2020-12-23 15:32:28 +00:00
u , err = user . GetUserWithEmail ( s , & user . User {
2020-11-21 16:38:58 +00:00
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 ,
2021-12-12 11:35:13 +00:00
Name : cl . Name ,
2021-07-13 20:56:02 +00:00
Status : user . StatusActive ,
2020-11-21 16:38:58 +00:00
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 , "-" )
}
2020-12-23 15:32:28 +00:00
u , err = user . CreateUser ( s , uu )
2020-11-21 16:38:58 +00:00
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 , "-" )
2020-12-23 15:32:28 +00:00
u , err = user . CreateUser ( s , uu )
2020-11-21 16:38:58 +00:00
if err != nil {
return nil , err
}
}
// And create its namespace
2020-12-23 15:32:28 +00:00
err = models . CreateNewNamespaceForUser ( s , u )
2020-11-21 16:38:58 +00:00
if err != nil {
return nil , err
}
return
}
// If it exists, check if the email address changed and change it if not
2020-11-21 20:51:55 +00:00
if cl . Email != u . Email || cl . Name != u . Name {
if cl . Email != u . Email {
u . Email = cl . Email
}
if cl . Name != u . Name {
u . Name = cl . Name
}
2020-12-23 15:32:28 +00:00
u , err = user . UpdateUser ( s , & user . User {
2020-11-21 16:38:58 +00:00
ID : u . ID ,
2020-11-21 20:51:55 +00:00
Email : u . Email ,
Name : u . Name ,
2020-11-21 16:38:58 +00:00
Issuer : issuer ,
Subject : subject ,
2023-01-23 17:30:01 +00:00
} , false )
2020-11-21 16:38:58 +00:00
if err != nil {
return nil , err
}
}
return
}