ResolveUser callback
Callbacks.ResolveUser is how you create or update users in your own database (GORM, raw SQL, external CRM) while still using goauth for sessions and OAuth.
When it runs
After default resolveUser logic (for database strategy: lookup by account, create user, link account). Always called if set — for both JWT and database strategies.
ResolveUser: func(ctx context.Context, p goauth.ResolveUserParams) (*goauth.User, error) {
// return the canonical user that goes into the session
},
ResolveUserParams
type ResolveUserParams struct {
User *User // mapped from provider or adapter
Account *Account // OAuth account metadata
Profile Profile // raw userinfo JSON
ProviderTokens *TokenSet // access/refresh/id tokens (OAuth only)
IsNewUser bool // adapter created a new user (database strategy)
}
| Field | Use it to |
|---|---|
User | Seed email, name, image for upsert |
Account | Key on (provider, providerAccountId) |
Profile | Read provider-specific fields |
ProviderTokens | Store refresh token, call Graph API |
IsNewUser | Send welcome email, onboarding |
Default behavior (ResolveUser nil)
Database strategy + adapter
flowchart TD
A[OAuth callback] --> B{GetUserByAccount?}
B -->|found| C[Use existing user]
B -->|not found| D{Email exists?}
D -->|yes| E[ErrOAuthAccountNotLinked]
D -->|no| F[CreateUser in adapter]
F --> G[LinkAccount]
G --> H[Events.CreateUser + LinkAccount]
JWT strategy (no adapter)
Profile User is used as-is — IsNewUser is always false.
Example: upsert into your users table (simple)
type AppUser struct {
ID string `gorm:"primaryKey"`
Email string `gorm:"unique"`
Name string
}
Callbacks: goauth.Callbacks{
ResolveUser: func(ctx context.Context, p goauth.ResolveUserParams) (*goauth.User, error) {
var u AppUser
err := gormDB.WithContext(ctx).
Where("email = ?", p.User.Email).
Assign(AppUser{Name: p.User.Name}).
FirstOrCreate(&u).Error
if err != nil {
return nil, err
}
return &goauth.User{ID: u.ID, Email: u.Email, Name: u.Name}, nil
},
},
Critical: Return User.ID that your app uses everywhere. That ID becomes the JWT sub claim and session user.id.
Example: new user onboarding (advanced)
ResolveUser: func(ctx context.Context, p goauth.ResolveUserParams) (*goauth.User, error) {
u, err := appDB.UpsertOAuthUser(ctx, UpsertInput{
Email: p.User.Email,
Name: p.User.Name,
Image: p.User.Image,
Provider: p.Account.Provider,
AccountID: p.Account.ProviderAccountID,
})
if err != nil {
return nil, err
}
if p.IsNewUser {
go welcomeEmail(u.Email) // async; don't block
}
return &goauth.User{ID: u.ID, Email: u.Email, Name: u.Name, Image: u.Image}, nil
},
Pair with Events.CreateUser if you still use goauth adapter tables and your own DB:
Events: goauth.Events{
CreateUser: func(ctx context.Context, user *goauth.User) {
log.Printf("goauth adapter created user %s", user.ID)
},
},
Example: store Microsoft Graph tokens (Azure AD)
ResolveUser: func(ctx context.Context, p goauth.ResolveUserParams) (*goauth.User, error) {
if p.ProviderTokens != nil && p.Account != nil && p.Account.Provider == "azuread" {
_ = tokenStore.Save(ctx, TokenRecord{
UserEmail: p.User.Email,
AccessToken: p.ProviderTokens.AccessToken,
RefreshToken: p.ProviderTokens.RefreshToken,
ExpiresAt: p.ProviderTokens.ExpiresAt,
Raw: p.ProviderTokens.Raw,
})
}
return persistUser(ctx, p.User)
},
Call Graph later with stored AccessToken.
Example: credentials — custom user IDs
Credentials skip adapter user creation. Use ResolveUser to map email → your DB id:
Providers: []goauth.Provider{
credentials.New(credentials.Options{
Authorize: func(ctx context.Context, creds map[string]string, r *http.Request) (*goauth.User, error) {
u, err := appDB.UserByEmail(ctx, creds["email"])
if err != nil || !checkPassword(u, creds["password"]) {
return nil, nil
}
// temporary ID until ResolveUser — or return DB user directly
return &goauth.User{ID: u.ID, Email: u.Email}, nil
},
}),
},
Callbacks: goauth.Callbacks{
ResolveUser: func(ctx context.Context, p goauth.ResolveUserParams) (*goauth.User, error) {
// reload fresh row, attach roles, etc.
u, err := appDB.UserByID(ctx, p.User.ID)
if err != nil {
return nil, err
}
return &goauth.User{ID: u.ID, Email: u.Email, Name: u.Name}, nil
},
},
Email / OTP user creation (no ResolveUser)
Magic link and OTP providers auto-create users in the adapter when email is unknown:
// actions_email.go — inside emailCallback
user, err := adapter.GetUserByEmail(ctx, identifier)
if user == nil {
user, err = adapter.CreateUser(ctx, &User{Email: identifier, EmailVerified: &now})
Events.CreateUser(ctx, user)
}
Use ResolveUser if you want email sign-in to write to your schema instead of only goauth_users.
Common mistakes
| Mistake | Fix |
|---|---|
Returning nil user without error | Return explicit error |
| Different ID in ResolveUser vs Authorize | Use one canonical ID |
| Heavy work in ResolveUser | Keep fast; use Events for async |
Ignoring ErrOAuthAccountNotLinked | Link accounts in UI or use same email policy |
See also: User creation guide.