mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-02-12 16:12:44 +00:00
only require needed for oauth import
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -22,7 +22,7 @@ type OAuthProvider struct {
|
||||
// oauth endpoints (user configurable)
|
||||
AuthURL string `gorm:"not null;type:varchar(512);"`
|
||||
TokenURL string `gorm:"not null;type:varchar(512);"`
|
||||
Scopes string `gorm:"not null;type:varchar(512);"`
|
||||
Scopes string `gorm:"not null;type:varchar(2048);"`
|
||||
|
||||
// user's oauth app credentials (stored as plain text like smtp passwords)
|
||||
ClientID string `gorm:"not null;type:varchar(255);"`
|
||||
|
||||
@@ -7,37 +7,25 @@ import (
|
||||
|
||||
// ImportAuthorizedToken represents an imported oauth token
|
||||
type ImportAuthorizedToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ClientID string `json:"client_id"`
|
||||
ExpiresAt int64 `json:"expires_at"` // unix timestamp in milliseconds
|
||||
Name string `json:"name"`
|
||||
User string `json:"user"`
|
||||
Scope string `json:"scope"`
|
||||
ExpiresAt int64 `json:"expires_at,omitempty"` // unix timestamp in milliseconds
|
||||
Name string `json:"name,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
TokenURL string `json:"token_url,omitempty"`
|
||||
CreatedAt int64 `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks if the imported token has a valid state
|
||||
func (i *ImportAuthorizedToken) Validate() error {
|
||||
if i.AccessToken == "" {
|
||||
return validate.WrapErrorWithField(errors.New("is required"), "access_token")
|
||||
}
|
||||
if i.RefreshToken == "" {
|
||||
return validate.WrapErrorWithField(errors.New("is required"), "refresh_token")
|
||||
}
|
||||
if i.Name == "" {
|
||||
return validate.WrapErrorWithField(errors.New("is required"), "name")
|
||||
}
|
||||
if i.ExpiresAt == 0 {
|
||||
return validate.WrapErrorWithField(errors.New("is required"), "expires_at")
|
||||
}
|
||||
if i.ClientID == "" {
|
||||
return validate.WrapErrorWithField(errors.New("is required"), "client_id")
|
||||
}
|
||||
if i.Scope == "" {
|
||||
return validate.WrapErrorWithField(errors.New("is required"), "scope")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ type OAuthProvider struct {
|
||||
Name nullable.Nullable[vo.String127] `json:"name"`
|
||||
|
||||
// oauth endpoints (user configurable)
|
||||
AuthURL nullable.Nullable[vo.String512] `json:"authURL"`
|
||||
TokenURL nullable.Nullable[vo.String512] `json:"tokenURL"`
|
||||
Scopes nullable.Nullable[vo.String512] `json:"scopes"`
|
||||
AuthURL nullable.Nullable[vo.String512] `json:"authURL"`
|
||||
TokenURL nullable.Nullable[vo.String512] `json:"tokenURL"`
|
||||
Scopes nullable.Nullable[vo.String2048] `json:"scopes"`
|
||||
|
||||
// user's oauth app credentials
|
||||
ClientID nullable.Nullable[vo.String255] `json:"clientID"`
|
||||
|
||||
@@ -219,7 +219,7 @@ func ToOAuthProvider(row *database.OAuthProvider) *model.OAuthProvider {
|
||||
name := nullable.NewNullableWithValue(*vo.NewString127Must(row.Name))
|
||||
authURL := nullable.NewNullableWithValue(*vo.NewString512Must(row.AuthURL))
|
||||
tokenURL := nullable.NewNullableWithValue(*vo.NewString512Must(row.TokenURL))
|
||||
scopes := nullable.NewNullableWithValue(*vo.NewString512Must(row.Scopes))
|
||||
scopes := nullable.NewNullableWithValue(*vo.NewString2048Must(row.Scopes))
|
||||
clientID := nullable.NewNullableWithValue(*vo.NewString255Must(row.ClientID))
|
||||
clientSecret := nullable.NewNullableWithValue(*vo.NewOptionalString255Must(row.ClientSecret))
|
||||
accessToken := nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(row.AccessToken))
|
||||
|
||||
@@ -217,7 +217,7 @@ func (o *OAuthProvider) UpdateByID(
|
||||
// clear all fields except name and id
|
||||
provider.AuthURL = nullable.NewNullNullable[vo.String512]()
|
||||
provider.TokenURL = nullable.NewNullNullable[vo.String512]()
|
||||
provider.Scopes = nullable.NewNullNullable[vo.String512]()
|
||||
provider.Scopes = nullable.NewNullNullable[vo.String2048]()
|
||||
provider.ClientID = nullable.NewNullNullable[vo.String255]()
|
||||
provider.ClientSecret = nullable.NewNullNullable[vo.OptionalString255]()
|
||||
provider.AccessToken = nullable.NewNullNullable[vo.OptionalString1MB]()
|
||||
@@ -603,11 +603,16 @@ func (o *OAuthProvider) getValidAccessTokenInternal(
|
||||
|
||||
data := url.Values{
|
||||
"client_id": {clientID.String()},
|
||||
"client_secret": {clientSecret},
|
||||
"refresh_token": {refreshToken.String()},
|
||||
"grant_type": {"refresh_token"},
|
||||
}
|
||||
|
||||
// only include client_secret if it's not a placeholder (imported tokens use "n/a")
|
||||
// public clients don't need/have client secrets
|
||||
if clientSecret != "" && clientSecret != "n/a" {
|
||||
data.Set("client_secret", clientSecret)
|
||||
}
|
||||
|
||||
newTokens, err := o.requestTokens(tokenURL.String(), data)
|
||||
if err != nil {
|
||||
o.Logger.Errorw("failed to refresh tokens", "error", err)
|
||||
@@ -697,19 +702,66 @@ func (o *OAuthProvider) ImportAuthorizedTokens(
|
||||
// set default token url if not provided
|
||||
token.SetDefaultTokenURL()
|
||||
|
||||
// convert expires_at from milliseconds to time
|
||||
expiresAt := time.UnixMilli(token.ExpiresAt)
|
||||
// generate name if empty
|
||||
if token.Name == "" {
|
||||
randomName, err := random.GenerateRandomURLBase64Encoded(16)
|
||||
if err != nil {
|
||||
o.Logger.Errorw("failed to generate random name for imported token", "error", err)
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
token.Name = fmt.Sprintf("imported-%s", randomName)
|
||||
}
|
||||
|
||||
// refresh token to get fresh access token and metadata
|
||||
// note: don't send client_secret for imported tokens (public clients don't have/need it)
|
||||
tokenURL := token.TokenURL
|
||||
clientID := token.ClientID
|
||||
data := url.Values{
|
||||
"client_id": {clientID},
|
||||
"refresh_token": {token.RefreshToken},
|
||||
"grant_type": {"refresh_token"},
|
||||
}
|
||||
|
||||
o.Logger.Debugw("refreshing token during import", "name", token.Name)
|
||||
newTokens, err := o.requestTokens(tokenURL, data)
|
||||
if err != nil {
|
||||
o.Logger.Errorw("failed to refresh token during import", "error", err, "name", token.Name)
|
||||
return nil, fmt.Errorf("failed to refresh token for '%s': %w", token.Name, err)
|
||||
}
|
||||
|
||||
// use refreshed access token
|
||||
accessToken := newTokens.AccessToken
|
||||
|
||||
// some providers return new refresh token, some don't
|
||||
refreshToken := newTokens.RefreshToken
|
||||
if refreshToken == "" {
|
||||
// keep the original refresh token
|
||||
refreshToken = token.RefreshToken
|
||||
}
|
||||
|
||||
// calculate expiry from refresh response
|
||||
expiresAt := time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second)
|
||||
|
||||
// use scope from refresh response if available, otherwise use provided scope
|
||||
// if both are empty, use placeholder to satisfy validation
|
||||
scope := newTokens.Scope
|
||||
if scope == "" {
|
||||
scope = token.Scope
|
||||
}
|
||||
if scope == "" {
|
||||
scope = "offline_access" // placeholder scope if none provided
|
||||
}
|
||||
|
||||
// create provider with imported flag
|
||||
provider := &model.OAuthProvider{
|
||||
Name: nullable.NewNullableWithValue(*vo.NewString127Must(token.Name)),
|
||||
AuthURL: nullable.NewNullableWithValue(*vo.NewString512Must("n/a")), // placeholder for imported
|
||||
TokenURL: nullable.NewNullableWithValue(*vo.NewString512Must(token.TokenURL)),
|
||||
Scopes: nullable.NewNullableWithValue(*vo.NewString512Must(token.Scope)),
|
||||
Scopes: nullable.NewNullableWithValue(*vo.NewString2048Must(scope)),
|
||||
ClientID: nullable.NewNullableWithValue(*vo.NewString255Must(token.ClientID)),
|
||||
ClientSecret: nullable.NewNullableWithValue(*vo.NewOptionalString255Must("n/a")), // placeholder for imported
|
||||
AccessToken: nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(token.AccessToken)),
|
||||
RefreshToken: nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(token.RefreshToken)),
|
||||
AccessToken: nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(accessToken)),
|
||||
RefreshToken: nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(refreshToken)),
|
||||
TokenExpiresAt: &expiresAt,
|
||||
AuthorizedEmail: nullable.NewNullableWithValue(*vo.NewOptionalString255Must(token.User)),
|
||||
AuthorizedAt: ptrTime(time.Now()),
|
||||
|
||||
@@ -552,6 +552,113 @@ func (s OptionalString1024) String() string {
|
||||
return s.inner
|
||||
}
|
||||
|
||||
// String2048 is a trimmed string with a min of 1 and a max of 2048
|
||||
type String2048 struct {
|
||||
inner string
|
||||
}
|
||||
|
||||
// NewString2048 creates a new long string
|
||||
func NewString2048(s string) (*String2048, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
err := validate.ErrorIfStringNotbetweenOrEqualTo(s, 1, 2048)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &String2048{
|
||||
inner: s,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewString2048Must creates a new long string and panics if it fails
|
||||
func NewString2048Must(s string) *String2048 {
|
||||
a, err := NewString2048(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface
|
||||
func (s String2048) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.inner)
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals the json into a string
|
||||
func (s *String2048) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return err
|
||||
}
|
||||
ss, err := NewString2048(str)
|
||||
if err != nil {
|
||||
return unwrapError(err)
|
||||
}
|
||||
s.inner = ss.inner
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string representation of the long string
|
||||
func (s String2048) String() string {
|
||||
return s.inner
|
||||
}
|
||||
|
||||
// OptionalString2048 is a trimmed string with a min of 0 and a max of 2048
|
||||
type OptionalString2048 struct {
|
||||
inner string
|
||||
}
|
||||
|
||||
// NewEmptyOptionalString2048 creates a new empty string
|
||||
func NewEmptyOptionalString2048() *OptionalString2048 {
|
||||
return &OptionalString2048{
|
||||
inner: "",
|
||||
}
|
||||
}
|
||||
|
||||
// NewOptionalString2048 creates a new long string
|
||||
func NewOptionalString2048(s string) (*OptionalString2048, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
err := validate.ErrorIfStringNotbetweenOrEqualTo(s, 0, 2048)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &OptionalString2048{
|
||||
inner: s,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewOptionalString2048Must creates a new long string and panics if it fails
|
||||
func NewOptionalString2048Must(s string) *OptionalString2048 {
|
||||
a, err := NewOptionalString2048(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface
|
||||
func (s OptionalString2048) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.inner)
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals the json into a string
|
||||
func (s *OptionalString2048) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return err
|
||||
}
|
||||
ss, err := NewOptionalString2048(str)
|
||||
if err != nil {
|
||||
return unwrapError(err)
|
||||
}
|
||||
s.inner = ss.inner
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string representation of the long string
|
||||
func (s OptionalString2048) String() string {
|
||||
return s.inner
|
||||
}
|
||||
|
||||
// String1MB is a trimmed string with a min of 1 and a max of 1000000
|
||||
type String1MB struct {
|
||||
inner string
|
||||
|
||||
Reference in New Issue
Block a user