Skip to main content

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)
}
FieldUse it to
UserSeed email, name, image for upsert
AccountKey on (provider, providerAccountId)
ProfileRead provider-specific fields
ProviderTokensStore refresh token, call Graph API
IsNewUserSend 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

MistakeFix
Returning nil user without errorReturn explicit error
Different ID in ResolveUser vs AuthorizeUse one canonical ID
Heavy work in ResolveUserKeep fast; use Events for async
Ignoring ErrOAuthAccountNotLinkedLink accounts in UI or use same email policy

See also: User creation guide.