✨ New ticket ways
This commit is contained in:
parent
0d78f34535
commit
87cccefddb
20
.idea/dataSources.local.xml
generated
Normal file
20
.idea/dataSources.local.xml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="dataSourceStorageLocal" created-in="GO-241.14494.238">
|
||||
<data-source name="hy_passport@localhost" uuid="74bcf3ef-a2b9-435b-b9e5-f32902a33b25">
|
||||
<database-info product="PostgreSQL" version="16.2 (Homebrew)" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.6.0" dbms="POSTGRES" exact-version="16.2" exact-driver-version="42.6">
|
||||
<identifier-quote-string>"</identifier-quote-string>
|
||||
</database-info>
|
||||
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
|
||||
<secret-storage>master_key</secret-storage>
|
||||
<user-name>postgres</user-name>
|
||||
<schema-mapping>
|
||||
<introspection-scope>
|
||||
<node kind="database" qname="@">
|
||||
<node kind="schema" qname="@" />
|
||||
</node>
|
||||
</introspection-scope>
|
||||
</schema-mapping>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="hy_passport@localhost" uuid="74bcf3ef-a2b9-435b-b9e5-f32902a33b25">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://localhost:5432/hy_passport</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
5716
.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25.xml
generated
Normal file
5716
.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25.xml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
#n:hy_passport
|
@ -0,0 +1,2 @@
|
||||
#n:public
|
||||
!<md> [6258, 0, null, null, -2147483648, -2147483648]
|
96
.idea/workspace.xml
generated
96
.idea/workspace.xml
generated
@ -4,73 +4,12 @@
|
||||
<option name="autoReloadType" value="ALL" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/pkg/embed.go" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/i18n/bundle.go" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/i18n/embed.go" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/i18n/locale.en.json" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/i18n/middleware.go" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/server/ui/auth.go" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/server/ui/index.go" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/views/index.gohtml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/views/partials/header.gohtml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pkg/views/signin.gohtml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.name" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/Identity.iml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/Project.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/dataSources.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/go.sum" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/cmd/main.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/cmd/main.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/meta.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/meta.go" afterDir="false" />
|
||||
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: New ticket ways">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/server/oauth_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/oauth_api.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/server/security_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/security_api.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/server/startup.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/startup.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/server/userinfo..go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/userinfo.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/.eslintrc.cjs" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/.eslintrc.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/.gitignore" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/.prettierrc.json" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/.vscode/extensions.json" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/README.md" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/embed.go" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/env.d.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/index.html" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/package.json" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/public/favicon.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/assets/utils.css" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/components/Copyright.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/components/NotificationList.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/components/UserMenu.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/components/auth/AccountLocator.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/components/auth/CallbackNotify.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/components/auth/FactorApplicator.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/components/auth/FactorPicker.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/index.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/layouts/master.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/layouts/user-center.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/main.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/router/index.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/scripts/request.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/stores/notifications.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/stores/userinfo.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/auth/claims.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/auth/connect.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/auth/sign-in.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/auth/sign-up.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/confirm.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/dashboard.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/personal-page.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/personalize.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/src/views/security.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/tsconfig.app.json" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/tsconfig.json" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/tsconfig.node.json" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/uno.config.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/views/vite.config.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pkg/services/ticket.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/ticket.go" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@ -93,10 +32,13 @@
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"customColor": "",
|
||||
"associatedIndex": 6
|
||||
}]]></component>
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="ProjectErrors" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 6
|
||||
}</component>
|
||||
<component name="ProjectId" id="2fLXu43fjlLYVIGNrhGhOgBFq2O" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
@ -118,14 +60,23 @@
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"run.code.analysis.last.selected.profile": "pProject Default",
|
||||
"settings.editor.selected.configurable": "preferences.lookFeel",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
]
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/pkg" />
|
||||
</key>
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/pkg/services" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration name="Backend" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
@ -161,6 +112,11 @@
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value=":recycle: Refactor frontend" />
|
||||
<MESSAGE value=":sparkles: New ticket ways" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: New ticket ways" />
|
||||
</component>
|
||||
<component name="VgoProject">
|
||||
<settings-migrated>true</settings-migrated>
|
||||
</component>
|
||||
|
@ -12,8 +12,7 @@ var DatabaseAutoActionRange = []any{
|
||||
&models.AccountPage{},
|
||||
&models.AccountContact{},
|
||||
&models.AccountFriendship{},
|
||||
&models.AuthSession{},
|
||||
&models.AuthChallenge{},
|
||||
&models.AuthTicket{},
|
||||
&models.MagicToken{},
|
||||
&models.ThirdClient{},
|
||||
&models.ActionEvent{},
|
||||
|
@ -23,8 +23,7 @@ type Account struct {
|
||||
PersonalPage AccountPage `json:"personal_page"`
|
||||
Contacts []AccountContact `json:"contacts"`
|
||||
|
||||
Sessions []AuthSession `json:"sessions"`
|
||||
Challenges []AuthChallenge `json:"challenges"`
|
||||
Sessions []AuthTicket `json:"sessions"`
|
||||
Factors []AuthFactor `json:"factors"`
|
||||
|
||||
Events []ActionEvent `json:"events"`
|
||||
|
@ -23,15 +23,19 @@ type AuthFactor struct {
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
||||
|
||||
type AuthSession struct {
|
||||
type AuthTicket struct {
|
||||
BaseModel
|
||||
|
||||
Location string `json:"location"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
RequireMFA bool `json:"require_mfa"`
|
||||
RequireAuthenticate bool `json:"require_authenticate"`
|
||||
Claims datatypes.JSONSlice[string] `json:"claims"`
|
||||
Audiences datatypes.JSONSlice[string] `json:"audiences"`
|
||||
Challenge AuthChallenge `json:"challenge" gorm:"foreignKey:SessionID"`
|
||||
GrantToken string `json:"grant_token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
GrantToken *string `json:"grant_token"`
|
||||
AccessToken *string `json:"access_token"`
|
||||
RefreshToken *string `json:"refresh_token"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
AvailableAt *time.Time `json:"available_at"`
|
||||
LastGrantAt *time.Time `json:"last_grant_at"`
|
||||
@ -39,7 +43,10 @@ type AuthSession struct {
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
||||
|
||||
func (v AuthSession) IsAvailable() error {
|
||||
func (v AuthTicket) IsAvailable() error {
|
||||
if v.RequireMFA || v.RequireAuthenticate {
|
||||
return fmt.Errorf("session isn't authenticated yet")
|
||||
}
|
||||
if v.AvailableAt != nil && time.Now().Unix() < v.AvailableAt.Unix() {
|
||||
return fmt.Errorf("session isn't available yet")
|
||||
}
|
||||
@ -50,40 +57,8 @@ func (v AuthSession) IsAvailable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthChallengeState = int8
|
||||
|
||||
const (
|
||||
ActiveChallengeState = AuthChallengeState(iota)
|
||||
ExpiredChallengeState
|
||||
FinishChallengeState
|
||||
)
|
||||
|
||||
type AuthChallenge struct {
|
||||
BaseModel
|
||||
|
||||
Location string `json:"location"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
RiskLevel int `json:"risk_level"`
|
||||
Progress int `json:"progress"`
|
||||
Requirements int `json:"requirements"`
|
||||
BlacklistFactors datatypes.JSONType[[]uint] `json:"blacklist_factors"`
|
||||
State int8 `json:"state"`
|
||||
ExpiredAt time.Time `json:"expired_at"`
|
||||
SessionID *uint `json:"session_id"`
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
||||
|
||||
func (v AuthChallenge) IsAvailable() error {
|
||||
if time.Now().Unix() > v.ExpiredAt.Unix() {
|
||||
return fmt.Errorf("challenge expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthContext struct {
|
||||
Session AuthSession `json:"session"`
|
||||
Ticket AuthTicket `json:"session"`
|
||||
Account Account `json:"account"`
|
||||
ExpiredAt time.Time `json:"expired_at"`
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ type ThirdClient struct {
|
||||
Secret string `json:"secret"`
|
||||
Urls datatypes.JSONSlice[string] `json:"urls"`
|
||||
Callbacks datatypes.JSONSlice[string] `json:"callbacks"`
|
||||
Sessions []AuthSession `json:"sessions" gorm:"foreignKey:ClientID"`
|
||||
Sessions []AuthTicket `json:"sessions" gorm:"foreignKey:ClientID"`
|
||||
Notifications []Notification `json:"notifications" gorm:"foreignKey:SenderID"`
|
||||
IsDraft bool `json:"is_draft"`
|
||||
AccountID *uint `json:"account_id"`
|
||||
|
@ -1,96 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
func CalcRisk(user models.Account, ip, ua string) int {
|
||||
risk := 3
|
||||
var secureFactor int64
|
||||
if err := database.C.Where(models.AuthChallenge{
|
||||
AccountID: user.ID,
|
||||
IpAddress: ip,
|
||||
}).Model(models.AuthChallenge{}).Count(&secureFactor).Error; err == nil {
|
||||
if secureFactor >= 3 {
|
||||
risk -= 3
|
||||
} else if secureFactor >= 1 {
|
||||
risk -= 2
|
||||
}
|
||||
}
|
||||
|
||||
return risk
|
||||
}
|
||||
|
||||
func NewChallenge(user models.Account, factors []models.AuthFactor, ip, ua string) (models.AuthChallenge, error) {
|
||||
var challenge models.AuthChallenge
|
||||
// Pickup any challenge if possible
|
||||
if err := database.C.Where(models.AuthChallenge{
|
||||
AccountID: user.ID,
|
||||
}).Where("state = ?", models.ActiveChallengeState).First(&challenge).Error; err == nil {
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
// Calculate the risk level
|
||||
risk := CalcRisk(user, ip, ua)
|
||||
|
||||
// Clamp risk in the exists requirements factor count
|
||||
requirements := lo.Clamp(risk, 1, len(factors))
|
||||
|
||||
challenge = models.AuthChallenge{
|
||||
IpAddress: ip,
|
||||
UserAgent: ua,
|
||||
RiskLevel: risk,
|
||||
Requirements: requirements,
|
||||
BlacklistFactors: datatypes.NewJSONType([]uint{}),
|
||||
State: models.ActiveChallengeState,
|
||||
ExpiredAt: time.Now().Add(2 * time.Hour),
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&challenge).Error
|
||||
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
func DoChallenge(challenge models.AuthChallenge, factor models.AuthFactor, code string) error {
|
||||
if err := challenge.IsAvailable(); err != nil {
|
||||
challenge.State = models.ExpiredChallengeState
|
||||
database.C.Save(&challenge)
|
||||
return err
|
||||
}
|
||||
if challenge.Progress >= challenge.Requirements {
|
||||
return fmt.Errorf("challenge already passed")
|
||||
}
|
||||
|
||||
blacklist := challenge.BlacklistFactors.Data()
|
||||
if lo.Contains(blacklist, factor.ID) {
|
||||
return fmt.Errorf("factor in blacklist, please change another factor to challenge")
|
||||
}
|
||||
if err := VerifyFactor(factor, code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
challenge.Progress++
|
||||
challenge.BlacklistFactors = datatypes.NewJSONType(append(blacklist, factor.ID))
|
||||
|
||||
if err := database.C.Save(&challenge).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Revoke some factor passwords
|
||||
if factor.Type == models.EmailPasswordFactor {
|
||||
factor.Secret = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||
database.C.Save(&factor)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func VerifyFactor(factor models.AuthFactor, code string) error {
|
||||
switch factor.Type {
|
||||
case models.PasswordAuthFactor:
|
||||
return lo.Ternary(
|
||||
VerifyPassword(code, factor.Secret),
|
||||
nil,
|
||||
fmt.Errorf("invalid password"),
|
||||
)
|
||||
case models.EmailPasswordFactor:
|
||||
return lo.Ternary(
|
||||
code == factor.Secret,
|
||||
nil,
|
||||
fmt.Errorf("invalid verification code"),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GrantSession(challenge models.AuthChallenge, claims, audiences []string, expired, available *time.Time) (models.AuthSession, error) {
|
||||
var session models.AuthSession
|
||||
if err := challenge.IsAvailable(); err != nil {
|
||||
return session, err
|
||||
}
|
||||
if challenge.Progress < challenge.Requirements {
|
||||
return session, fmt.Errorf("challenge haven't passed")
|
||||
}
|
||||
|
||||
challenge.State = models.FinishChallengeState
|
||||
|
||||
session = models.AuthSession{
|
||||
Claims: claims,
|
||||
Audiences: audiences,
|
||||
Challenge: challenge,
|
||||
GrantToken: uuid.NewString(),
|
||||
AccessToken: uuid.NewString(),
|
||||
RefreshToken: uuid.NewString(),
|
||||
ExpiredAt: expired,
|
||||
AvailableAt: available,
|
||||
AccountID: challenge.AccountID,
|
||||
}
|
||||
|
||||
if err := database.C.Save(&challenge).Error; err != nil {
|
||||
return session, err
|
||||
} else if err := database.C.Save(&session).Error; err != nil {
|
||||
return session, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func GrantOauthSession(user models.Account, client models.ThirdClient, claims, audiences []string, expired, available *time.Time, ip, ua string) (models.AuthSession, error) {
|
||||
session := models.AuthSession{
|
||||
Claims: claims,
|
||||
Audiences: audiences,
|
||||
Challenge: models.AuthChallenge{
|
||||
IpAddress: ip,
|
||||
UserAgent: ua,
|
||||
RiskLevel: CalcRisk(user, ip, ua),
|
||||
State: models.FinishChallengeState,
|
||||
AccountID: user.ID,
|
||||
},
|
||||
GrantToken: uuid.NewString(),
|
||||
AccessToken: uuid.NewString(),
|
||||
RefreshToken: uuid.NewString(),
|
||||
ExpiredAt: expired,
|
||||
AvailableAt: available,
|
||||
ClientID: &client.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
if err := database.C.Save(&session).Error; err != nil {
|
||||
return session, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func RegenSession(session models.AuthSession) (models.AuthSession, error) {
|
||||
session.GrantToken = uuid.NewString()
|
||||
session.AccessToken = uuid.NewString()
|
||||
session.RefreshToken = uuid.NewString()
|
||||
err := database.C.Save(&session).Error
|
||||
return session, err
|
||||
}
|
||||
|
||||
func GetToken(session models.AuthSession) (string, string, error) {
|
||||
var refresh, access string
|
||||
if err := session.IsAvailable(); err != nil {
|
||||
return refresh, access, err
|
||||
}
|
||||
|
||||
accessDuration := time.Duration(viper.GetInt64("security.access_token_duration")) * time.Second
|
||||
refreshDuration := time.Duration(viper.GetInt64("security.refresh_token_duration")) * time.Second
|
||||
|
||||
var err error
|
||||
sub := strconv.Itoa(int(session.AccountID))
|
||||
sed := strconv.Itoa(int(session.ID))
|
||||
access, err = EncodeJwt(session.AccessToken, JwtAccessType, sub, sed, session.Audiences, time.Now().Add(accessDuration))
|
||||
if err != nil {
|
||||
return refresh, access, err
|
||||
}
|
||||
refresh, err = EncodeJwt(session.RefreshToken, JwtRefreshType, sub, sed, session.Audiences, time.Now().Add(refreshDuration))
|
||||
if err != nil {
|
||||
return refresh, access, err
|
||||
}
|
||||
|
||||
session.LastGrantAt = lo.ToPtr(time.Now())
|
||||
database.C.Save(&session)
|
||||
|
||||
return access, refresh, nil
|
||||
}
|
||||
|
||||
func ExchangeToken(token string) (string, string, error) {
|
||||
var session models.AuthSession
|
||||
if err := database.C.Where(models.AuthSession{GrantToken: token}).First(&session).Error; err != nil {
|
||||
return "404", "403", err
|
||||
} else if session.LastGrantAt != nil {
|
||||
return "404", "403", fmt.Errorf("session was granted the first token, use refresh token instead")
|
||||
} else if len(session.Audiences) > 1 {
|
||||
return "404", "403", fmt.Errorf("should use authorization code grant type")
|
||||
}
|
||||
|
||||
return GetToken(session)
|
||||
}
|
||||
|
||||
func ExchangeOauthToken(clientId, clientSecret, redirectUri, token string) (string, string, error) {
|
||||
var client models.ThirdClient
|
||||
if err := database.C.Where(models.ThirdClient{Alias: clientId}).First(&client).Error; err != nil {
|
||||
return "404", "403", err
|
||||
} else if client.Secret != clientSecret {
|
||||
return "404", "403", fmt.Errorf("invalid client secret")
|
||||
} else if !client.IsDraft && !lo.Contains(client.Callbacks, redirectUri) {
|
||||
return "404", "403", fmt.Errorf("invalid redirect uri")
|
||||
}
|
||||
|
||||
var session models.AuthSession
|
||||
if err := database.C.Where(models.AuthSession{GrantToken: token}).First(&session).Error; err != nil {
|
||||
return "404", "403", err
|
||||
} else if session.LastGrantAt != nil {
|
||||
return "404", "403", fmt.Errorf("session was granted the first token, use refresh token instead")
|
||||
}
|
||||
|
||||
return GetToken(session)
|
||||
}
|
||||
|
||||
func RefreshToken(token string) (string, string, error) {
|
||||
parseInt := func(str string) int {
|
||||
val, _ := strconv.Atoi(str)
|
||||
return val
|
||||
}
|
||||
|
||||
var session models.AuthSession
|
||||
if claims, err := DecodeJwt(token); err != nil {
|
||||
return "404", "403", err
|
||||
} else if claims.Type != JwtRefreshType {
|
||||
return "404", "403", fmt.Errorf("invalid token type, expected refresh token")
|
||||
} else if err := database.C.Where(models.AuthSession{
|
||||
BaseModel: models.BaseModel{ID: uint(parseInt(claims.SessionID))},
|
||||
}).First(&session).Error; err != nil {
|
||||
return "404", "403", err
|
||||
}
|
||||
|
||||
if session, err := RegenSession(session); err != nil {
|
||||
return "404", "403", err
|
||||
} else {
|
||||
return GetToken(session)
|
||||
}
|
||||
}
|
@ -114,7 +114,7 @@ func killSession(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("sessionId", 0)
|
||||
|
||||
if err := database.C.Delete(&models.AuthSession{}, &models.AuthSession{
|
||||
if err := database.C.Delete(&models.AuthTicket{}, &models.AuthTicket{
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
AccountID: user.ID,
|
||||
}).Error; err != nil {
|
||||
|
145
pkg/server/auth_api.go
Normal file
145
pkg/server/auth_api.go
Normal file
@ -0,0 +1,145 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
)
|
||||
|
||||
func doAuthenticate(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := services.LookupAccount(data.Username)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
|
||||
}
|
||||
|
||||
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid password: %v", err.Error()))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": ticket.IsAvailable(),
|
||||
"ticket": ticket,
|
||||
})
|
||||
}
|
||||
|
||||
func doMultiFactorAuthenticate(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
TicketID uint `json:"ticket_id" validate:"required"`
|
||||
FactorID uint `json:"factor_id" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := services.GetTicket(data.TicketID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
factor, err := services.GetFactor(data.FactorID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("factor was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
ticket, err = services.ActiveTicketWithMFA(ticket, factor, data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid code: %v", err.Error()))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": ticket.IsAvailable(),
|
||||
"ticket": ticket,
|
||||
})
|
||||
}
|
||||
|
||||
func getToken(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Code string `json:"code" form:"code"`
|
||||
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
||||
ClientID string `json:"client_id" form:"client_id"`
|
||||
ClientSecret string `json:"client_secret" form:"client_secret"`
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
|
||||
GrantType string `json:"grant_type" form:"grant_type"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var access, refresh string
|
||||
switch data.GrantType {
|
||||
case "refresh_token":
|
||||
// Refresh Token
|
||||
access, refresh, err = services.RefreshToken(data.RefreshToken)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "authorization_code":
|
||||
// Authorization Code Mode
|
||||
access, refresh, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "password":
|
||||
// Password Mode
|
||||
user, err := services.LookupAccount(data.Username)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
|
||||
}
|
||||
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
|
||||
}
|
||||
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid password: %v", err.Error()))
|
||||
} else if ticket.GrantToken == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get grant token to get token"))
|
||||
}
|
||||
access, refresh, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, *ticket.GrantToken)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "grant_token":
|
||||
// Internal Usage
|
||||
access, refresh, err = services.ExchangeToken(data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
default:
|
||||
return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type")
|
||||
}
|
||||
|
||||
services.SetJwtCookieSet(c, access, refresh)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"id_token": access,
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": (30 * time.Minute).Seconds(),
|
||||
})
|
||||
}
|
@ -3,14 +3,13 @@ package server
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/security"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func authMiddleware(c *fiber.Ctx) error {
|
||||
var token string
|
||||
if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 {
|
||||
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
|
||||
token = cookie
|
||||
}
|
||||
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
|
||||
@ -42,10 +41,10 @@ func authFunc(c *fiber.Ctx, overrides ...string) error {
|
||||
}
|
||||
}
|
||||
|
||||
rtk := c.Cookies(security.CookieRefreshKey)
|
||||
rtk := c.Cookies(services.CookieRefreshKey)
|
||||
if user, atk, rtk, err := services.Authenticate(token, rtk, 0); err == nil {
|
||||
if atk != token {
|
||||
security.SetJwtCookieSet(c, atk, rtk)
|
||||
services.SetJwtCookieSet(c, atk, rtk)
|
||||
}
|
||||
c.Locals("principal", user)
|
||||
return nil
|
||||
|
@ -1,140 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/security"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func startChallenge(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := services.LookupAccount(data.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
factors, err := services.LookupFactorsByUser(user.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
challenge, err := security.NewChallenge(user, factors, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
services.AddEvent(user, "challenges.start", data.ID, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
return c.JSON(fiber.Map{
|
||||
"display_name": user.Nick,
|
||||
"challenge": challenge,
|
||||
"factors": factors,
|
||||
})
|
||||
}
|
||||
|
||||
func doChallenge(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
ChallengeID uint `json:"challenge_id" validate:"required"`
|
||||
FactorID uint `json:"factor_id" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
challenge, err := services.LookupChallengeWithFingerprint(data.ChallengeID, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
factor, err := services.LookupFactor(data.FactorID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := security.DoChallenge(challenge, factor, data.Secret); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
challenge, err = services.LookupChallenge(data.ChallengeID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else if challenge.Progress >= challenge.Requirements {
|
||||
session, err := security.GrantSession(challenge, []string{"*"}, []string{"passport"}, nil, lo.ToPtr(time.Now()))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": true,
|
||||
"challenge": challenge,
|
||||
"session": session,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": false,
|
||||
"challenge": challenge,
|
||||
"session": nil,
|
||||
})
|
||||
}
|
||||
|
||||
func exchangeToken(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Code string `json:"code" form:"code"`
|
||||
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
||||
ClientID string `json:"client_id" form:"client_id"`
|
||||
ClientSecret string `json:"client_secret" form:"client_secret"`
|
||||
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
|
||||
GrantType string `json:"grant_type" form:"grant_type"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var access, refresh string
|
||||
switch data.GrantType {
|
||||
case "authorization_code":
|
||||
// Authorization Code Mode
|
||||
access, refresh, err = security.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "grant_token":
|
||||
// Internal Usage
|
||||
access, refresh, err = security.ExchangeToken(data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "refresh_token":
|
||||
// Refresh Token
|
||||
access, refresh, err = security.RefreshToken(data.RefreshToken)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
default:
|
||||
return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type")
|
||||
}
|
||||
|
||||
security.SetJwtCookieSet(c, access, refresh)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"id_token": access,
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": (30 * time.Minute).Seconds(),
|
||||
})
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
func requestFactorToken(c *fiber.Ctx) error {
|
||||
id, _ := c.ParamsInt("factorId", 0)
|
||||
|
||||
factor, err := services.LookupFactor(uint(id))
|
||||
factor, err := services.GetFactor(uint(id))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/security"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
@ -29,8 +28,8 @@ func preConnect(c *fiber.Ctx) error {
|
||||
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var session models.AuthSession
|
||||
if err := database.C.Where(&models.AuthSession{
|
||||
var session models.AuthTicket
|
||||
if err := database.C.Where(&models.AuthTicket{
|
||||
AccountID: user.ID,
|
||||
ClientID: &client.ID,
|
||||
}).Where("last_grant_at IS NULL").First(&session).Error; err == nil {
|
||||
@ -40,7 +39,7 @@ func preConnect(c *fiber.Ctx) error {
|
||||
"session": nil,
|
||||
})
|
||||
} else {
|
||||
session, err = security.RegenSession(session)
|
||||
session, err = services.RegenSession(session)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
@ -73,13 +72,11 @@ func doConnect(c *fiber.Ctx) error {
|
||||
switch response {
|
||||
case "code":
|
||||
// OAuth Authorization Mode
|
||||
session, err := security.GrantOauthSession(
|
||||
ticket, err := services.NewOauthTicket(
|
||||
user,
|
||||
client,
|
||||
strings.Split(scope, " "),
|
||||
[]string{"passport", client.Alias},
|
||||
nil,
|
||||
lo.ToPtr(time.Now()),
|
||||
c.IP(),
|
||||
c.Get(fiber.HeaderUserAgent),
|
||||
)
|
||||
@ -89,26 +86,24 @@ func doConnect(c *fiber.Ctx) error {
|
||||
} else {
|
||||
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
return c.JSON(fiber.Map{
|
||||
"session": session,
|
||||
"session": ticket,
|
||||
"redirect_uri": redirect,
|
||||
})
|
||||
}
|
||||
case "token":
|
||||
// OAuth Implicit Mode
|
||||
session, err := security.GrantOauthSession(
|
||||
ticket, err := services.NewOauthTicket(
|
||||
user,
|
||||
client,
|
||||
strings.Split(scope, " "),
|
||||
[]string{"passport", client.Alias},
|
||||
nil,
|
||||
lo.ToPtr(time.Now()),
|
||||
c.IP(),
|
||||
c.Get(fiber.HeaderUserAgent),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else if access, refresh, err := security.GetToken(session); err != nil {
|
||||
} else if access, refresh, err := services.GetToken(ticket); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
@ -116,7 +111,7 @@ func doConnect(c *fiber.Ctx) error {
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"redirect_uri": redirect,
|
||||
"session": session,
|
||||
"ticket": ticket,
|
||||
})
|
||||
}
|
||||
default:
|
||||
|
@ -6,52 +6,23 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getChallenges(c *fiber.Ctx) error {
|
||||
func getTickets(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
var count int64
|
||||
var challenges []models.AuthChallenge
|
||||
var sessions []models.AuthTicket
|
||||
if err := database.C.
|
||||
Where(&models.AuthChallenge{AccountID: user.ID}).
|
||||
Model(&models.AuthChallenge{}).
|
||||
Where(&models.AuthTicket{AccountID: user.ID}).
|
||||
Model(&models.AuthTicket{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := database.C.
|
||||
Order("created_at desc").
|
||||
Where(&models.AuthChallenge{AccountID: user.ID}).
|
||||
Limit(take).
|
||||
Offset(offset).
|
||||
Find(&challenges).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": challenges,
|
||||
})
|
||||
}
|
||||
|
||||
func getSessions(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
var count int64
|
||||
var sessions []models.AuthSession
|
||||
if err := database.C.
|
||||
Where(&models.AuthSession{AccountID: user.ID}).
|
||||
Model(&models.AuthSession{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := database.C.
|
||||
Order("created_at desc").
|
||||
Where(&models.AuthSession{AccountID: user.ID}).
|
||||
Where(&models.AuthTicket{AccountID: user.ID}).
|
||||
Limit(take).
|
||||
Offset(offset).
|
||||
Find(&sessions).Error; err != nil {
|
||||
|
@ -87,8 +87,7 @@ func NewServer() {
|
||||
me.Put("/", authMiddleware, editUserinfo)
|
||||
me.Put("/page", authMiddleware, editPersonalPage)
|
||||
me.Get("/events", authMiddleware, getEvents)
|
||||
me.Get("/challenges", authMiddleware, getChallenges)
|
||||
me.Get("/sessions", authMiddleware, getSessions)
|
||||
me.Get("/tickets", authMiddleware, getTickets)
|
||||
me.Delete("/sessions/:sessionId", authMiddleware, killSession)
|
||||
|
||||
me.Post("/confirm", doRegisterConfirm)
|
||||
@ -112,9 +111,8 @@ func NewServer() {
|
||||
|
||||
api.Post("/users", doRegister)
|
||||
|
||||
api.Put("/auth", startChallenge)
|
||||
api.Post("/auth", doChallenge)
|
||||
api.Post("/auth/token", exchangeToken)
|
||||
api.Post("/auth", doAuthenticate)
|
||||
api.Post("/auth/token", getToken)
|
||||
api.Post("/auth/factors/:factorId", requestFactorToken)
|
||||
|
||||
api.Get("/auth/o/connect", authMiddleware, preConnect)
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/security"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
@ -23,14 +22,14 @@ func GetAccount(id uint) (models.Account, error) {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func LookupAccount(id string) (models.Account, error) {
|
||||
func LookupAccount(probe string) (models.Account, error) {
|
||||
var account models.Account
|
||||
if err := database.C.Where(models.Account{Name: id}).First(&account).Error; err == nil {
|
||||
if err := database.C.Where(models.Account{Name: probe}).First(&account).Error; err == nil {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
var contact models.AccountContact
|
||||
if err := database.C.Where(models.AccountContact{Content: id}).First(&contact).Error; err == nil {
|
||||
if err := database.C.Where(models.AccountContact{Content: probe}).First(&contact).Error; err == nil {
|
||||
if err := database.C.
|
||||
Where(models.Account{
|
||||
BaseModel: models.BaseModel{ID: contact.AccountID},
|
||||
@ -52,7 +51,7 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
|
||||
Factors: []models.AuthFactor{
|
||||
{
|
||||
Type: models.PasswordAuthFactor,
|
||||
Secret: security.HashPassword(password),
|
||||
Secret: HashPassword(password),
|
||||
},
|
||||
{
|
||||
Type: models.EmailPasswordFactor,
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/security"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -16,12 +15,12 @@ import (
|
||||
const authContextBucket = "AuthContext"
|
||||
|
||||
func Authenticate(access, refresh string, depth int) (user models.Account, newAccess, newRefresh string, err error) {
|
||||
var claims security.PayloadClaims
|
||||
claims, err = security.DecodeJwt(access)
|
||||
var claims PayloadClaims
|
||||
claims, err = DecodeJwt(access)
|
||||
if err != nil {
|
||||
if len(refresh) > 0 && depth < 1 {
|
||||
// Auto refresh and retry
|
||||
newAccess, newRefresh, err = security.RefreshToken(refresh)
|
||||
newAccess, newRefresh, err = RefreshToken(refresh)
|
||||
if err == nil {
|
||||
return Authenticate(newAccess, newRefresh, depth+1)
|
||||
}
|
||||
@ -74,7 +73,7 @@ func GetAuthContext(jti string) (models.AuthContext, error) {
|
||||
})
|
||||
|
||||
if err == nil && time.Now().Unix() >= ctx.ExpiredAt.Unix() {
|
||||
RevokeAuthContext(jti)
|
||||
_ = RevokeAuthContext(jti)
|
||||
|
||||
return ctx, fmt.Errorf("auth context has been expired")
|
||||
}
|
||||
@ -86,7 +85,7 @@ func GrantAuthContext(jti string) (models.AuthContext, error) {
|
||||
var ctx models.AuthContext
|
||||
|
||||
// Query data from primary database
|
||||
session, err := LookupSessionWithToken(jti)
|
||||
session, err := GetTicketWithToken(jti)
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("invalid auth session: %v", err)
|
||||
} else if err := session.IsAvailable(); err != nil {
|
||||
@ -101,7 +100,7 @@ func GrantAuthContext(jti string) (models.AuthContext, error) {
|
||||
// Every context should expires in some while
|
||||
// Once user update their account info, this will have delay to update
|
||||
ctx = models.AuthContext{
|
||||
Session: session,
|
||||
Ticket: session,
|
||||
Account: user,
|
||||
ExpiredAt: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
)
|
||||
|
||||
func LookupChallenge(id uint) (models.AuthChallenge, error) {
|
||||
var challenge models.AuthChallenge
|
||||
err := database.C.Where(models.AuthChallenge{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
}).First(&challenge).Error
|
||||
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
func LookupChallengeWithFingerprint(id uint, ip string, ua string) (models.AuthChallenge, error) {
|
||||
var challenge models.AuthChallenge
|
||||
err := database.C.Where(models.AuthChallenge{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
IpAddress: ip,
|
||||
UserAgent: ua,
|
||||
}).First(&challenge).Error
|
||||
|
||||
return challenge, err
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package security
|
||||
package services
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
@ -24,7 +25,17 @@ Thank you for your cooperation in helping us maintain the security of your accou
|
||||
Best regards,
|
||||
%s`
|
||||
|
||||
func LookupFactor(id uint) (models.AuthFactor, error) {
|
||||
func GetPasswordFactor(userId uint) (models.AuthFactor, error) {
|
||||
var factor models.AuthFactor
|
||||
err := database.C.Where(models.AuthFactor{
|
||||
Type: models.PasswordAuthFactor,
|
||||
AccountID: userId,
|
||||
}).First(&factor).Error
|
||||
|
||||
return factor, err
|
||||
}
|
||||
|
||||
func GetFactor(id uint) (models.AuthFactor, error) {
|
||||
var factor models.AuthFactor
|
||||
err := database.C.Where(models.AuthFactor{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
@ -33,10 +44,10 @@ func LookupFactor(id uint) (models.AuthFactor, error) {
|
||||
return factor, err
|
||||
}
|
||||
|
||||
func LookupFactorsByUser(uid uint) ([]models.AuthFactor, error) {
|
||||
func ListUserFactor(userId uint) ([]models.AuthFactor, error) {
|
||||
var factors []models.AuthFactor
|
||||
err := database.C.Where(models.AuthFactor{
|
||||
AccountID: uid,
|
||||
AccountID: userId,
|
||||
}).Find(&factors).Error
|
||||
|
||||
return factors, err
|
||||
@ -68,3 +79,22 @@ func GetFactorCode(factor models.AuthFactor) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func CheckFactor(factor models.AuthFactor, code string) error {
|
||||
switch factor.Type {
|
||||
case models.PasswordAuthFactor:
|
||||
return lo.Ternary(
|
||||
VerifyPassword(code, factor.Secret),
|
||||
nil,
|
||||
fmt.Errorf("invalid password"),
|
||||
)
|
||||
case models.EmailPasswordFactor:
|
||||
return lo.Ternary(
|
||||
code == factor.Secret,
|
||||
nil,
|
||||
fmt.Errorf("invalid verification code"),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package security
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,28 +1,15 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.etcd.io/bbolt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LookupSessionWithToken(tokenId string) (models.AuthSession, error) {
|
||||
var session models.AuthSession
|
||||
if err := database.C.
|
||||
Where(models.AuthSession{AccessToken: tokenId}).
|
||||
Or(models.AuthSession{RefreshToken: tokenId}).
|
||||
First(&session).Error; err != nil {
|
||||
return session, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func DoAutoSignoff() {
|
||||
duration := time.Duration(viper.GetInt64("security.auto_signoff_duration")) * time.Second
|
||||
divider := time.Now().Add(-duration)
|
||||
@ -31,7 +18,7 @@ func DoAutoSignoff() {
|
||||
|
||||
if tx := database.C.
|
||||
Where("last_grant_at < ?", divider).
|
||||
Delete(&models.AuthSession{}); tx.Error != nil {
|
||||
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.")
|
136
pkg/services/ticket.go
Normal file
136
pkg/services/ticket.go
Normal file
@ -0,0 +1,136 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func DetectRisk(user models.Account, ip, ua string) bool {
|
||||
var secureFactor int64
|
||||
if err := database.C.Where(models.AuthTicket{
|
||||
AccountID: user.ID,
|
||||
IpAddress: ip,
|
||||
}).Model(models.AuthTicket{}).Count(&secureFactor).Error; err == nil {
|
||||
if secureFactor >= 1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func NewTicket(user models.Account, ip, ua string) (models.AuthTicket, error) {
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.Where(models.AuthTicket{
|
||||
AccountID: user.ID,
|
||||
}).First(&ticket).Error; err == nil {
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
ticket = models.AuthTicket{
|
||||
Claims: []string{"*"},
|
||||
Audiences: []string{"passport"},
|
||||
IpAddress: ip,
|
||||
UserAgent: ua,
|
||||
RequireMFA: DetectRisk(user, ip, ua),
|
||||
ExpiredAt: lo.ToPtr(time.Now().Add(2 * time.Hour)),
|
||||
AvailableAt: nil,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&ticket).Error
|
||||
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
func NewOauthTicket(
|
||||
user models.Account,
|
||||
client models.ThirdClient,
|
||||
claims, audiences []string,
|
||||
ip, ua string,
|
||||
) (models.AuthTicket, error) {
|
||||
ticket := models.AuthTicket{
|
||||
Claims: claims,
|
||||
Audiences: audiences,
|
||||
IpAddress: ip,
|
||||
UserAgent: ua,
|
||||
RequireMFA: DetectRisk(user, ip, ua),
|
||||
GrantToken: lo.ToPtr(uuid.NewString()),
|
||||
AccessToken: lo.ToPtr(uuid.NewString()),
|
||||
RefreshToken: lo.ToPtr(uuid.NewString()),
|
||||
AvailableAt: lo.ToPtr(time.Now()),
|
||||
ExpiredAt: lo.ToPtr(time.Now()),
|
||||
ClientID: &client.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
if err := database.C.Save(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func ActiveTicketWithPassword(ticket models.AuthTicket, password string) (models.AuthTicket, error) {
|
||||
if ticket.AvailableAt != nil {
|
||||
return ticket, nil
|
||||
} else if !ticket.RequireAuthenticate {
|
||||
return ticket, fmt.Errorf("detected risk, multi factor authentication required")
|
||||
}
|
||||
|
||||
if factor, err := GetPasswordFactor(ticket.AccountID); err != nil {
|
||||
return ticket, fmt.Errorf("unable to active ticket: %v", err)
|
||||
} else if err = CheckFactor(factor, password); err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
||||
|
||||
if !ticket.RequireAuthenticate && !ticket.RequireMFA {
|
||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
||||
}
|
||||
|
||||
if err := database.C.Save(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func ActiveTicketWithMFA(ticket models.AuthTicket, factor models.AuthFactor, code string) (models.AuthTicket, error) {
|
||||
if ticket.AvailableAt != nil {
|
||||
return ticket, nil
|
||||
} else if !ticket.RequireMFA {
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
if err := CheckFactor(factor, code); err != nil {
|
||||
return ticket, fmt.Errorf("invalid code: %v", err)
|
||||
}
|
||||
|
||||
ticket.RequireMFA = false
|
||||
|
||||
if !ticket.RequireAuthenticate && !ticket.RequireMFA {
|
||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
||||
}
|
||||
|
||||
if err := database.C.Save(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func RegenSession(session models.AuthTicket) (models.AuthTicket, error) {
|
||||
session.GrantToken = lo.ToPtr(uuid.NewString())
|
||||
session.AccessToken = lo.ToPtr(uuid.NewString())
|
||||
session.RefreshToken = lo.ToPtr(uuid.NewString())
|
||||
err := database.C.Save(&session).Error
|
||||
return session, err
|
||||
}
|
29
pkg/services/ticket_queries.go
Normal file
29
pkg/services/ticket_queries.go
Normal file
@ -0,0 +1,29 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
)
|
||||
|
||||
func GetTicket(id uint) (models.AuthTicket, error) {
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.
|
||||
Where(&models.AuthTicket{BaseModel: models.BaseModel{ID: id}}).
|
||||
First(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func GetTicketWithToken(tokenId string) (models.AuthTicket, error) {
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.
|
||||
Where(models.AuthTicket{AccessToken: &tokenId}).
|
||||
Or(models.AuthTicket{RefreshToken: &tokenId}).
|
||||
First(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
98
pkg/services/ticket_token.go
Normal file
98
pkg/services/ticket_token.go
Normal file
@ -0,0 +1,98 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetToken(ticket models.AuthTicket) (string, string, error) {
|
||||
var refresh, access string
|
||||
if err := ticket.IsAvailable(); err != nil {
|
||||
return refresh, access, err
|
||||
}
|
||||
if ticket.AccessToken == nil || ticket.RefreshToken == nil {
|
||||
return refresh, access, fmt.Errorf("unable to encode token, access or refresh token id missing")
|
||||
}
|
||||
|
||||
accessDuration := time.Duration(viper.GetInt64("access_token_duration")) * time.Second
|
||||
refreshDuration := time.Duration(viper.GetInt64("refresh_token_duration")) * time.Second
|
||||
|
||||
var err error
|
||||
sub := strconv.Itoa(int(ticket.AccountID))
|
||||
sed := strconv.Itoa(int(ticket.ID))
|
||||
access, err = EncodeJwt(*ticket.AccessToken, JwtAccessType, sub, sed, ticket.Audiences, time.Now().Add(accessDuration))
|
||||
if err != nil {
|
||||
return refresh, access, err
|
||||
}
|
||||
refresh, err = EncodeJwt(*ticket.RefreshToken, JwtRefreshType, sub, sed, ticket.Audiences, time.Now().Add(refreshDuration))
|
||||
if err != nil {
|
||||
return refresh, access, err
|
||||
}
|
||||
|
||||
ticket.LastGrantAt = lo.ToPtr(time.Now())
|
||||
database.C.Save(&ticket)
|
||||
|
||||
return access, refresh, nil
|
||||
}
|
||||
|
||||
func ExchangeToken(token string) (string, string, error) {
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.Where(models.AuthTicket{GrantToken: &token}).First(&ticket).Error; err != nil {
|
||||
return "", "", err
|
||||
} else if ticket.LastGrantAt != nil {
|
||||
return "", "", fmt.Errorf("ticket was granted the first token, use refresh token instead")
|
||||
} else if len(ticket.Audiences) > 1 {
|
||||
return "", "", fmt.Errorf("should use authorization code grant type")
|
||||
}
|
||||
|
||||
return GetToken(ticket)
|
||||
}
|
||||
|
||||
func ExchangeOauthToken(clientId, clientSecret, redirectUri, token string) (string, string, error) {
|
||||
var client models.ThirdClient
|
||||
if err := database.C.Where(models.ThirdClient{Alias: clientId}).First(&client).Error; err != nil {
|
||||
return "", "", err
|
||||
} else if client.Secret != clientSecret {
|
||||
return "", "", fmt.Errorf("invalid client secret")
|
||||
} else if !client.IsDraft && !lo.Contains(client.Callbacks, redirectUri) {
|
||||
return "", "", fmt.Errorf("invalid redirect uri")
|
||||
}
|
||||
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.Where(models.AuthTicket{GrantToken: &token}).First(&ticket).Error; err != nil {
|
||||
return "", "", err
|
||||
} else if ticket.LastGrantAt != nil {
|
||||
return "", "", fmt.Errorf("ticket was granted the first token, use refresh token instead")
|
||||
}
|
||||
|
||||
return GetToken(ticket)
|
||||
}
|
||||
|
||||
func RefreshToken(token string) (string, string, error) {
|
||||
parseInt := func(str string) int {
|
||||
val, _ := strconv.Atoi(str)
|
||||
return val
|
||||
}
|
||||
|
||||
var ticket models.AuthTicket
|
||||
if claims, err := DecodeJwt(token); err != nil {
|
||||
return "404", "403", err
|
||||
} else if claims.Type != JwtRefreshType {
|
||||
return "404", "403", fmt.Errorf("invalid token type, expected refresh token")
|
||||
} else if err := database.C.Where(models.AuthTicket{
|
||||
BaseModel: models.BaseModel{ID: uint(parseInt(claims.SessionID))},
|
||||
}).First(&ticket).Error; err != nil {
|
||||
return "404", "403", err
|
||||
}
|
||||
|
||||
if ticket, err := RegenSession(ticket); err != nil {
|
||||
return "404", "403", err
|
||||
} else {
|
||||
return GetToken(ticket)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user