2023-12-24 10:18:08 +00:00
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
2020-05-04 14:58:02 +00:00
//
2023-12-24 10:18:08 +00:00
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
2020-05-04 14:58:02 +00:00
//
2023-12-24 10:18:08 +00:00
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
2020-05-04 14:58:02 +00:00
//
2023-12-24 10:18:08 +00:00
// 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 Affero General Public License for more details.
2020-05-04 14:58:02 +00:00
//
2023-12-24 10:18:08 +00:00
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2020-05-04 14:58:02 +00:00
2019-09-10 17:11:13 +00:00
package api
import (
2021-12-16 14:53:58 +00:00
"crypto/rand"
2020-05-12 16:17:14 +00:00
"encoding/base64"
2021-01-03 21:40:47 +00:00
"fmt"
2020-05-12 16:17:14 +00:00
"log"
2019-09-10 17:11:13 +00:00
"net/http"
2020-04-20 09:53:33 +00:00
"time"
2019-09-10 17:11:13 +00:00
2021-05-05 01:48:16 +00:00
"github.com/gin-gonic/gin"
2023-05-09 09:18:34 +00:00
"github.com/golang-jwt/jwt/v5"
2019-09-10 17:11:13 +00:00
2023-09-07 09:37:18 +00:00
"git.happydns.org/happyDomain/config"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/storage"
2019-09-10 17:11:13 +00:00
)
2021-01-03 21:40:47 +00:00
const NO_AUTH_ACCOUNT = "_no_auth"
2021-05-05 01:48:16 +00:00
func declareAuthenticationRoutes ( opts * config . Options , router * gin . RouterGroup ) {
router . POST ( "/auth" , func ( c * gin . Context ) {
2021-12-16 14:53:58 +00:00
checkAuth ( opts , c )
2021-05-05 01:48:16 +00:00
} )
router . POST ( "/auth/logout" , func ( c * gin . Context ) {
logout ( opts , c )
} )
apiAuthRoutes := router . Group ( "/auth" )
apiAuthRoutes . Use ( authMiddleware ( opts , true ) )
apiAuthRoutes . GET ( "" , func ( c * gin . Context ) {
if _ , exists := c . Get ( "MySession" ) ; exists {
displayAuthToken ( c )
} else {
displayNotAuthToken ( opts , c )
}
} )
2019-09-10 17:11:13 +00:00
}
2020-04-20 09:53:33 +00:00
type DisplayUser struct {
2023-08-05 16:15:52 +00:00
// Id is the user identifier
Id happydns . Identifier ` json:"id" swaggertype:"string" `
// Email is the user email.
Email string ` json:"email" `
// CreatedAt stores the date of the account creation.
CreatedAt time . Time ` json:"created_at,omitempty" `
// Settings holds the user configuration.
Settings happydns . UserSettings ` json:"settings,omitempty" `
2020-04-20 09:53:33 +00:00
}
2020-05-12 16:17:14 +00:00
func currentUser ( u * happydns . User ) * DisplayUser {
return & DisplayUser {
2021-12-16 14:53:58 +00:00
Id : u . Id ,
Email : u . Email ,
CreatedAt : u . CreatedAt ,
Settings : u . Settings ,
2021-01-03 21:40:47 +00:00
}
}
2023-08-05 16:15:52 +00:00
// displayAuthToken returns the user information.
//
// @Summary User info.
// @Schemes
// @Description Retrieve information about the currently logged user.
// @Tags user_auth
// @Accept json
// @Produce json
// @Security securitydefinitions.basic
// @Success 200 {object} DisplayUser
// @Failure 401 {object} happydns.Error "Authentication failure"
// @Router /auth [get]
2021-05-05 01:48:16 +00:00
func displayAuthToken ( c * gin . Context ) {
user := c . MustGet ( "LoggedUser" ) . ( * happydns . User )
c . JSON ( http . StatusOK , currentUser ( user ) )
2020-05-12 16:17:14 +00:00
}
2023-09-07 08:20:50 +00:00
func displayNotAuthToken ( opts * config . Options , c * gin . Context ) * UserClaims {
2021-12-16 14:53:58 +00:00
if ! opts . NoAuth {
2021-12-16 18:21:30 +00:00
requireLogin ( opts , c , "Authorization required" )
2023-09-07 08:20:50 +00:00
return nil
2020-05-12 16:17:14 +00:00
}
2021-12-16 14:53:58 +00:00
claims , err := completeAuth ( opts , c , UserProfile {
UserId : [ ] byte { 0 } ,
Email : NO_AUTH_ACCOUNT ,
EmailVerified : true ,
} )
if err != nil {
log . Printf ( "%s %s" , c . ClientIP ( ) , err . Error ( ) )
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : "Something went wrong during your authentication. Please retry in a few minutes" } )
2023-09-07 08:20:50 +00:00
return nil
2020-05-12 16:17:14 +00:00
}
2021-12-16 14:53:58 +00:00
realUser , err := retrieveUserFromClaims ( claims )
if err != nil {
c . JSON ( http . StatusOK , gin . H { "errmsg" : "Login success" } )
} else {
c . JSON ( http . StatusOK , currentUser ( realUser ) )
}
2023-09-07 08:20:50 +00:00
return claims
2020-05-12 16:17:14 +00:00
}
2023-08-05 16:15:52 +00:00
// logout closes the user session.
//
// @Summary Close session.
// @Schemes
// @Description Erase the HTTP-only cookie. This leads to user logout in its browser.
// @Tags user_auth
// @Accept json
// @Produce json
// @Success 204 {null} null "Loged out"
// @Failure 401 {object} happydns.Error "Authentication failure"
// @Router /auth/logout [post]
2021-05-05 01:48:16 +00:00
func logout ( opts * config . Options , c * gin . Context ) {
2021-07-06 16:34:36 +00:00
c . SetCookie (
COOKIE_NAME ,
"" ,
- 1 ,
opts . BaseURL + "/" ,
"" ,
2023-09-07 07:56:27 +00:00
opts . DevProxy == "" && opts . ExternalURL . URL . Scheme != "http" ,
2021-07-06 16:34:36 +00:00
true ,
)
2023-08-05 16:15:52 +00:00
c . Status ( http . StatusNoContent )
2019-09-10 17:11:13 +00:00
}
2023-09-07 10:22:33 +00:00
type LoginForm struct {
2023-08-05 16:15:52 +00:00
// Email of the user.
Email string
// Password of the user.
2019-09-10 17:11:13 +00:00
Password string
}
2023-08-05 16:15:52 +00:00
// checkAuth validate user authentication and delivers a session token.
//
// @Summary Authenticate user.
// @Schemes
// @Description Validate user authentication and delivers a session token.
// @Tags user_auth
// @Accept json
// @Produce json
2023-09-07 10:22:33 +00:00
// @Param body body LoginForm true "Login information"
2023-08-05 16:15:52 +00:00
// @Success 200 {object} DisplayUser "Login success"
// @Failure 401 {object} happydns.Error "Authentication failure"
// @Failure 500 {object} happydns.Error
// @Router /auth [post]
2021-05-05 01:48:16 +00:00
func checkAuth ( opts * config . Options , c * gin . Context ) {
2023-09-07 10:22:33 +00:00
var lf LoginForm
2021-05-05 01:48:16 +00:00
if err := c . ShouldBindJSON ( & lf ) ; err != nil {
2021-07-30 09:49:11 +00:00
log . Printf ( "%s sends invalid LoginForm JSON: %s" , c . ClientIP ( ) , err . Error ( ) )
c . AbortWithStatusJSON ( http . StatusBadRequest , gin . H { "errmsg" : fmt . Sprintf ( "Something is wrong in received data: %s" , err . Error ( ) ) } )
2021-05-05 01:48:16 +00:00
return
2019-09-10 17:11:13 +00:00
}
2021-12-16 14:53:58 +00:00
user , err := storage . MainStore . GetAuthUserByEmail ( lf . Email )
2021-05-05 01:48:16 +00:00
if err != nil {
2021-07-30 09:49:11 +00:00
log . Printf ( "%s user's email (%s) not found: %s" , c . ClientIP ( ) , lf . Email , err . Error ( ) )
2021-05-05 01:48:16 +00:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "errmsg" : "Invalid username or password." } )
return
2019-09-10 17:11:13 +00:00
}
2021-05-05 01:48:16 +00:00
if ! user . CheckAuth ( lf . Password ) {
log . Printf ( "%s tries to login as %q, but sent an invalid password" , c . ClientIP ( ) , lf . Email )
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "errmsg" : "Invalid username or password." } )
return
}
2021-12-16 14:53:58 +00:00
if user . EmailVerification == nil {
2021-05-05 01:48:16 +00:00
log . Printf ( "%s tries to login as %q, but sent an invalid password" , c . ClientIP ( ) , lf . Email )
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "errmsg" : "Please validate your e-mail address before your first login." , "href" : "/email-validation" } )
return
}
2021-12-16 14:53:58 +00:00
claims , err := completeAuth ( opts , c , UserProfile {
UserId : user . Id ,
Email : user . Email ,
EmailVerified : user . EmailVerification != nil ,
CreatedAt : user . CreatedAt ,
2023-11-19 13:18:56 +00:00
Newsletter : user . AllowCommercials ,
2021-12-16 14:53:58 +00:00
} )
if err != nil {
log . Printf ( "%s %s" , c . ClientIP ( ) , err . Error ( ) )
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : "Something went wrong during your authentication. Please retry in a few minutes" } )
return
}
log . Printf ( "%s now logged as %q\n" , c . ClientIP ( ) , user . Email )
realUser , err := retrieveUserFromClaims ( claims )
if err != nil {
c . JSON ( http . StatusOK , gin . H { "errmsg" : "Login success" } )
} else {
c . JSON ( http . StatusOK , currentUser ( realUser ) )
}
}
func completeAuth ( opts * config . Options , c * gin . Context , userprofile UserProfile ) ( * UserClaims , error ) {
// Issue a new JWT token
jti := make ( [ ] byte , 16 )
_ , err := rand . Read ( jti )
if err != nil {
return nil , fmt . Errorf ( "unable to read enough random bytes: %w" , err )
}
2023-05-20 10:43:32 +00:00
iat := jwt . NewNumericDate ( time . Now ( ) )
2021-12-16 14:53:58 +00:00
claims := & UserClaims {
userprofile ,
jwt . RegisteredClaims {
2023-05-20 10:43:32 +00:00
IssuedAt : iat ,
2021-12-16 14:53:58 +00:00
ID : base64 . StdEncoding . EncodeToString ( jti ) ,
} ,
}
jwtToken := jwt . NewWithClaims ( signingMethod , claims )
jwtToken . Header [ "kid" ] = "1"
token , err := jwtToken . SignedString ( [ ] byte ( opts . JWTSecretKey ) )
if err != nil {
return nil , fmt . Errorf ( "unable to sign user claims: %w" , err )
}
c . SetCookie (
COOKIE_NAME , // name
token , // value
30 * 24 * 3600 , // maxAge
opts . BaseURL + "/" , // path
"" , // domain
2023-09-07 07:56:27 +00:00
opts . DevProxy == "" && opts . ExternalURL . URL . Scheme != "http" , // secure
2021-12-16 14:53:58 +00:00
true , // httpOnly
)
return claims , nil
2019-09-10 17:11:13 +00:00
}