2020-05-04 14:58:02 +00:00
// Copyright or © or Copr. happyDNS (2020)
//
2022-01-10 13:06:19 +00:00
// contact@happydomain.org
2020-05-04 14:58:02 +00:00
//
// This software is a computer program whose purpose is to provide a modern
// interface to interact with DNS systems.
//
// This software is governed by the CeCILL license under French law and abiding
// by the rules of distribution of free software. You can use, modify and/or
// redistribute the software under the terms of the CeCILL license as
// circulated by CEA, CNRS and INRIA at the following URL
// "http://www.cecill.info".
//
// As a counterpart to the access to the source code and rights to copy, modify
// and redistribute granted by the license, users are provided only with a
// limited warranty and the software's author, the holder of the economic
// rights, and the successive licensors have only limited liability.
//
// In this respect, the user's attention is drawn to the risks associated with
// loading, using, modifying and/or developing or reproducing the software by
// the user in light of its specific status of free software, that may mean
// that it is complicated to manipulate, and that also therefore means that it
// is reserved for developers and experienced professionals having in-depth
// computer knowledge. Users are therefore encouraged to load and test the
// software's suitability as regards their requirements in conditions enabling
// the security of their systems and/or data to be ensured and, more generally,
// to use and operate it in the same conditions as regards security.
//
// The fact that you are presently reading this means that you have had
// knowledge of the CeCILL license and that you accept its terms.
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"
2021-07-06 16:34:36 +00:00
"strings"
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"
2021-12-16 14:53:58 +00:00
"github.com/golang-jwt/jwt/v4"
2019-09-10 17:11:13 +00:00
2022-01-10 13:06:19 +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 {
2022-10-25 16:16:34 +00:00
Id happydns . Identifier ` json:"id" `
2021-12-16 14:53:58 +00:00
Email string ` json:"email" `
CreatedAt time . Time ` json:"created_at,omitempty" `
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
}
}
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
}
2021-12-16 14:53:58 +00:00
func displayNotAuthToken ( opts * config . Options , c * gin . Context ) {
if ! opts . NoAuth {
2021-12-16 18:21:30 +00:00
requireLogin ( opts , c , "Authorization required" )
2021-05-05 01:48:16 +00:00
return
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" } )
2021-05-05 01:48:16 +00:00
return
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 ) )
}
2020-05-12 16:17:14 +00:00
}
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 + "/" ,
"" ,
opts . DevProxy == "" && ! strings . HasPrefix ( opts . ExternalURL , "http://" ) ,
true ,
)
2021-05-05 01:48:16 +00:00
c . JSON ( http . StatusOK , true )
2019-09-10 17:11:13 +00:00
}
type loginForm struct {
Email string
Password string
}
2021-05-05 01:48:16 +00:00
func checkAuth ( opts * config . Options , c * gin . Context ) {
2019-09-10 17:11:13 +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 ,
} )
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 )
}
iat := jwt . NumericDate { time . Now ( ) }
claims := & UserClaims {
userprofile ,
jwt . RegisteredClaims {
IssuedAt : & iat ,
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
opts . DevProxy == "" && ! strings . HasPrefix ( opts . ExternalURL , "http://" ) , // secure
true , // httpOnly
)
return claims , nil
2019-09-10 17:11:13 +00:00
}