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"
"math/rand"
"net/http"
"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"
2021-05-17 19:34:19 +00:00
"code.vikunja.io/api/pkg/user"
2020-11-21 16:38:58 +00:00
"github.com/coreos/go-oidc"
"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 )
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 {
if rerr , is := err . ( * oauth2 . RetrieveError ) ; is {
log . Error ( err )
details := make ( map [ string ] interface { } )
if err := json . Unmarshal ( rerr . Body , & details ) ; err != nil {
2021-05-16 11:23:10 +00:00
log . Errorf ( "Error unmarshaling token for provider %s: %v" , provider . Name , err )
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" } )
}
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 {
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 { }
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 )
}
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-11-26 02:25:57 +00:00
u , err := auth . GetOrCreateUserFromExternalAuth ( s , idToken . Issuer , idToken . Subject , cl . Email , cl . Name , cl . PreferredUsername )
2020-12-23 15:32:28 +00:00
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
}
err = s . Commit ( )
2020-11-21 16:38:58 +00:00
if err != nil {
2021-05-16 11:23:10 +00:00
return handler . HandleHTTPError ( err , c )
2020-11-21 16:38:58 +00:00
}
// Create token
return auth . NewUserAuthTokenResponse ( u , c )
}