2024-04-20 11:04:33 +00:00
package services
import (
"fmt"
2024-10-14 14:28:30 +00:00
"gorm.io/datatypes"
2024-04-20 11:04:33 +00:00
"time"
2024-09-15 18:37:02 +00:00
"github.com/rs/zerolog/log"
2024-04-20 11:04:33 +00:00
"github.com/google/uuid"
2024-06-17 14:21:34 +00:00
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
2024-04-20 11:04:33 +00:00
"github.com/samber/lo"
)
2024-08-12 12:58:20 +00:00
const InternalTokenAudience = "solar-network"
2024-07-28 11:50:49 +00:00
2024-10-13 06:02:48 +00:00
// DetectRisk is used for detect user environment is suitable for no multifactorial authenticating or not.
2024-09-15 18:37:02 +00:00
// Return the remaining steps, value is from 1 to 2, may appear 3 if user enabled the third-authentication-factor.
func DetectRisk ( user models . Account , ip , ua string ) int {
2024-06-26 07:17:10 +00:00
var clue int64
2024-04-30 17:33:11 +00:00
if err := database . C .
Where ( models . AuthTicket { AccountID : user . ID , IpAddress : ip } ) .
Where ( "available_at IS NOT NULL" ) .
Model ( models . AuthTicket { } ) .
2024-06-26 07:17:10 +00:00
Count ( & clue ) . Error ; err == nil {
if clue >= 1 {
2024-10-14 15:45:28 +00:00
return 1
2024-04-20 11:04:33 +00:00
}
}
2024-10-13 06:02:48 +00:00
return 3
2024-04-20 11:04:33 +00:00
}
2024-10-13 04:36:51 +00:00
// PickTicketAttempt is trying to pick up the ticket that hasn't completed but created by a same client (identify by ip address).
// Then the client can continue their journey to get ticket activated.
2024-09-15 18:37:02 +00:00
func PickTicketAttempt ( user models . Account , ip string ) ( models . AuthTicket , error ) {
2024-04-20 11:04:33 +00:00
var ticket models . AuthTicket
2024-04-20 17:33:42 +00:00
if err := database . C .
2024-09-15 18:37:02 +00:00
Where ( "account_id = ? AND ip_address = ? AND expired_at < ? AND available_at IS NULL" , user . ID , ip , time . Now ( ) ) .
First ( & ticket ) . Error ; err != nil {
return ticket , err
}
return ticket , nil
}
func NewTicket ( user models . Account , ip , ua string ) ( models . AuthTicket , error ) {
var ticket models . AuthTicket
if ticket , err := PickTicketAttempt ( user , ip ) ; err == nil {
2024-04-20 11:04:33 +00:00
return ticket , nil
}
2024-09-15 18:37:02 +00:00
steps := DetectRisk ( user , ip , ua )
if count := CountUserFactor ( user . ID ) ; count <= 0 {
return ticket , fmt . Errorf ( "specified user didn't enable sign in" )
} else {
steps = min ( steps , int ( count ) )
2024-10-13 04:36:51 +00:00
cfg , err := GetAuthPreference ( user )
if err == nil && cfg . Config . Data ( ) . MaximumAuthSteps >= 1 {
steps = min ( steps , cfg . Config . Data ( ) . MaximumAuthSteps )
2024-10-12 17:45:08 +00:00
}
2024-04-20 17:33:42 +00:00
}
2024-04-20 11:04:33 +00:00
ticket = models . AuthTicket {
2024-09-15 18:37:02 +00:00
Claims : [ ] string { "*" } ,
Audiences : [ ] string { InternalTokenAudience } ,
IpAddress : ip ,
UserAgent : ua ,
StepRemain : steps ,
ExpiredAt : nil ,
AvailableAt : nil ,
AccountID : user . ID ,
2024-04-20 11:04:33 +00:00
}
err := database . C . Save ( & ticket ) . Error
return ticket , err
}
func NewOauthTicket (
user models . Account ,
client models . ThirdClient ,
claims , audiences [ ] string ,
2024-07-28 14:30:51 +00:00
ip , ua string , nonce * string ,
2024-04-20 11:04:33 +00:00
) ( models . AuthTicket , error ) {
2024-07-28 14:30:51 +00:00
if nonce != nil && len ( * nonce ) == 0 {
nonce = nil
}
2024-04-20 11:04:33 +00:00
ticket := models . AuthTicket {
Claims : claims ,
Audiences : audiences ,
IpAddress : ip ,
UserAgent : ua ,
GrantToken : lo . ToPtr ( uuid . NewString ( ) ) ,
AccessToken : lo . ToPtr ( uuid . NewString ( ) ) ,
RefreshToken : lo . ToPtr ( uuid . NewString ( ) ) ,
AvailableAt : lo . ToPtr ( time . Now ( ) ) ,
2024-06-26 06:47:34 +00:00
ExpiredAt : lo . ToPtr ( time . Now ( ) . Add ( 7 * 24 * time . Hour ) ) ,
2024-07-28 14:30:51 +00:00
Nonce : nonce ,
2024-04-20 11:04:33 +00:00
ClientID : & client . ID ,
AccountID : user . ID ,
}
if err := database . C . Save ( & ticket ) . Error ; err != nil {
return ticket , err
}
return ticket , nil
}
2024-09-15 18:37:02 +00:00
func ActiveTicket ( ticket models . AuthTicket ) ( models . AuthTicket , error ) {
2024-04-20 11:04:33 +00:00
if ticket . AvailableAt != nil {
return ticket , nil
2024-09-15 18:37:02 +00:00
} else if err := ticket . IsCanBeAvailble ( ) ; err != nil {
return ticket , err
2024-04-20 11:04:33 +00:00
}
2024-09-15 18:37:02 +00:00
ticket . AvailableAt = lo . ToPtr ( time . Now ( ) )
ticket . GrantToken = lo . ToPtr ( uuid . NewString ( ) )
ticket . AccessToken = lo . ToPtr ( uuid . NewString ( ) )
ticket . RefreshToken = lo . ToPtr ( uuid . NewString ( ) )
if err := database . C . Save ( & ticket ) . Error ; err != nil {
2024-04-20 11:04:33 +00:00
return ticket , err
2024-10-14 14:28:30 +00:00
} else {
_ = NewNotification ( models . Notification {
Topic : "passport.security.alert" ,
Title : "New sign in alert" ,
Subtitle : lo . ToPtr ( fmt . Sprintf ( "New sign in from %s" , ticket . IpAddress ) ) ,
Body : fmt . Sprintf ( "Your account just got a new sign in from %s. Make sure you recongize this device, or sign out it immediately and reset password." , ticket . IpAddress ) ,
Metadata : datatypes . JSONMap {
"ip_address" : ticket . IpAddress ,
"created_at" : ticket . CreatedAt ,
"available_at" : ticket . AvailableAt ,
} ,
AccountID : ticket . AccountID ,
IsForcePush : true ,
} )
2024-04-20 11:04:33 +00:00
}
2024-09-15 18:37:02 +00:00
return ticket , nil
}
func ActiveTicketWithPassword ( ticket models . AuthTicket , password string ) ( models . AuthTicket , error ) {
if ticket . AvailableAt != nil {
return ticket , nil
} else if ticket . StepRemain == 1 {
return ticket , fmt . Errorf ( "multi-factor authentication required" )
}
2024-04-20 11:04:33 +00:00
2024-09-15 18:37:02 +00:00
factor , err := GetPasswordTypeFactor ( ticket . AccountID )
if err != nil {
return ticket , fmt . Errorf ( "unable to authenticate, password factor was not found: %v" , err )
} else if err := CheckFactor ( factor , password ) ; err != nil {
return ticket , fmt . Errorf ( "invalid password: %v" , err )
2024-04-20 11:04:33 +00:00
}
2024-09-15 18:37:02 +00:00
ticket . StepRemain --
ticket . FactorTrail = append ( ticket . FactorTrail , int ( factor . ID ) )
ticket . AvailableAt = lo . ToPtr ( time . Now ( ) )
ticket . GrantToken = lo . ToPtr ( uuid . NewString ( ) )
ticket . AccessToken = lo . ToPtr ( uuid . NewString ( ) )
ticket . RefreshToken = lo . ToPtr ( uuid . NewString ( ) )
2024-04-20 11:04:33 +00:00
if err := database . C . Save ( & ticket ) . Error ; err != nil {
return ticket , err
}
return ticket , nil
}
2024-09-15 18:37:02 +00:00
func PerformTicketCheck ( ticket models . AuthTicket , factor models . AuthFactor , code string ) ( models . AuthTicket , error ) {
2024-04-20 11:04:33 +00:00
if ticket . AvailableAt != nil {
return ticket , nil
2024-09-15 18:37:02 +00:00
} else if ticket . StepRemain <= 0 {
2024-04-20 11:04:33 +00:00
return ticket , nil
}
2024-09-15 18:37:02 +00:00
if lo . Contains ( ticket . FactorTrail , int ( factor . ID ) ) {
return ticket , fmt . Errorf ( "already checked this ticket with factor %d" , factor . ID )
}
2024-04-20 11:04:33 +00:00
if err := CheckFactor ( factor , code ) ; err != nil {
return ticket , fmt . Errorf ( "invalid code: %v" , err )
}
2024-09-15 18:37:02 +00:00
ticket . StepRemain --
ticket . FactorTrail = append ( ticket . FactorTrail , int ( factor . ID ) )
2024-04-20 11:04:33 +00:00
2024-09-15 18:37:02 +00:00
if ticket . IsCanBeAvailble ( ) == nil {
return ActiveTicket ( ticket )
} else {
if err := database . C . Save ( & ticket ) . Error ; err != nil {
return ticket , err
}
2024-04-20 11:04:33 +00:00
}
return ticket , nil
}
2024-08-24 15:49:19 +00:00
func RotateTicket ( ticket models . AuthTicket , fullyRestart ... bool ) ( models . AuthTicket , error ) {
2024-04-20 17:33:42 +00:00
ticket . GrantToken = lo . ToPtr ( uuid . NewString ( ) )
ticket . AccessToken = lo . ToPtr ( uuid . NewString ( ) )
ticket . RefreshToken = lo . ToPtr ( uuid . NewString ( ) )
2024-08-24 15:49:19 +00:00
if len ( fullyRestart ) > 0 && fullyRestart [ 0 ] {
ticket . LastGrantAt = nil
}
2024-04-20 17:33:42 +00:00
err := database . C . Save ( & ticket ) . Error
return ticket , err
2024-04-20 11:04:33 +00:00
}
2024-08-24 12:28:10 +00:00
func DoAutoSignoff ( ) {
duration := 7 * 24 * time . Hour
deadline := time . Now ( ) . Add ( - duration )
log . Debug ( ) . Time ( "before" , deadline ) . Msg ( "Now signing off tickets..." )
if tx := database . C .
Where ( "last_grant_at < ?" , deadline ) .
Delete ( & models . AuthTicket { } ) ; tx . Error != nil {
log . Error ( ) . Err ( tx . Error ) . Msg ( "An error occurred when running auto sign off..." )
} else {
log . Debug ( ) . Int64 ( "affected" , tx . RowsAffected ) . Msg ( "Auto sign off accomplished." )
}
}