gauth

package module
v0.0.0-...-ded3010 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 1, 2024 License: MIT Imports: 32 Imported by: 0

README

Run Tests

gauth

Login, registration library for go using your own models. This has a built-in UI for login, register and account pages. Optionally just use json endpoints. This is a standalone auth system that is embedded to your application.



Install

go get github.com/altlimit/gauth

Features

  • Registration forms with customizable input, identity, email field and password fields.
  • Login form with 2FA, recovery, inactive/verify email flow.
  • Passwordless login / sending login email link.
  • Forgot Password / Resetting password
  • Account page with customizable input and tabs, allow 2FA, password update, etc.
  • Customizable color scheme.

Examples

Refer to cmd/memory for a full example that stores new accounts in memory.

How To

This library comes with a form template that you can customize the color scheme to match with your application or simply just use the json endpoints.

You must implement the IdentityProvider interface to allow gauth to know how to load or save your account/user. In IdentityLoad it returns an Identity interface which usually is your user model. If you don't have one, you can make any struct that would be use to store your user properties.


// Here we create an Identity interface by adding "gauth" tag to map in the gauth.Fields you provided.
type User struct {
    ID            string
    Name          string `gauth:"name"`
    Password      string `gauth:"password"`
    Email         string `gauth:"email"`
    Active        bool   `gauth:"active"` // built-in tag
    TotpSecretKey string `gauth:"totpsecret"` // built-in tag
    RecoveryCodes string `gauth:"recoverycodes"` // built-in tag
}

func (u *User) IdentitySave(ctx context.Context) (string, error) {
    // once it reaches here, it's safe to save your user and return it's unique id
	return u.ID, nil
}

type identityProvider struct {

}

func (ip *identityProvider) IdentityUID(ctx context.Context, id string) (string, error) {
    // The id here is whatever you provided in IdentityFieldID - this could be email or username
    // if you support email link activation(by default it's enabled or if you have EmailFieldID provided)
    // and it's not yet active you must return ErrIdentityNotActive with the actual unique ID.
    if u != nil {
        if !u.Active {
            return u.ID, gauth.ErrIdentityNotActive
        }
        return u.ID, nil
    }
    // if user does not exists return ErrIdentityNotFound
	return "", gauth.ErrIdentityNotFound
}

func (ip *identityProvider) IdentityLoad(ctx context.Context, uid string) (gauth.Identity, error) {
    // You'll get the unique id you provided in IdentityUID here in uid and you should return a Identity,
    // your user model should implement the Identity interface like above with IdentitySave.
    // If the uid is an empty string return an empty struct for your model to be created later and ErrIdentityNotFound
	if uid == "" {
		return &User{}, gauth.ErrIdentityNotFound
	}
    // load user and return
	return u, nil
}

That's all you need, everything else will be optional.

Custom Tokens

You can customize how your refresh and access tokens are created. The default behaviour is that your refresh token will be a JWT that has a claim cid which is the sha1(IP+UserAgent+PasswordHash). This is invalidated by updating your password or logout will add a blacklist of cid for the last 1000 using in memory lru cache. Then your access token makes sure your cid matches before it returns the default access as grants which is also customizable. Without changing anything it's stateless but invalidation for logout on distributed systems will not be blocked until expiration.

// called when you login
func (ip *identityProvider) CreateRefreshToken(ctx context.Context, uid string) (string, error) {
    // if you need access to *http.Request it's in ctx.Value(gauth.RequestKey)
    loginID, err := newLoginForUID(ctx, uid)
	return loginID, err
}

// called when you logout
func (ip *identityProvider) DeleteRefreshToken(ctx context.Context, uid, cid string) error {
    // This is called on logout you should revoke your cid here, load your login and delete it
    return deleteLoginForUID(ctx, uid, cid)
}


// Implment AccessTokenProvider to customize the grants for your access token
type Permission struct {
    Owner bool    `json:"owner"`
    Roles []int64 `json:"role_ids"`
}

// called everytime you refresh and create access_token
func (ip *identityProvider) CreateAccessToken(ctx context.Context, uid string, cid string) (interface{}, error) {
    // check if your cid is still logged in
    if err := isUIDLoggedIn(ctx, uid, cid); err != nil {
        return nil, err
    }
    // load this users roles into your custom grants
	roles, err := loadUserRoles(ctx, uid)
	return Permission{
		Owner: false,
		Roles: []int64{1, 2, 3},
	}, nil
}

Once you have those implemented, you can either wrap any logged in page with AuthMiddleware or manually check with Authorized

ga := gauth.NewDefault("Example", "http://localhost:8888", &identityProvider{})
// provide JwtKey or it will generate a random key.
ga.JwtKey = FromYourConfig.JwtKey
http.Handle("/auth/", ga.MustInit(false))
// here your me handler must have Authorization: Bearer {accessToken} or it will return 401
http.Handle("/api/me", ga.AuthMiddleware(meHandler()))
// if you provide AccessTokenCookieName AuthMiddleware automatically redirects to refresh and
// create an access token stored in cookie for authorization through cookie
ga.AccessTokenCookieName = "atoken"
http.Handle("/dashboard", ga.AuthMiddleware(dashboardHandler()))

// you could also create your own middleware and use ga.Authorized(r) to check for auth
auth, err := ga.Authorized(r)
if err != nil {
    // not authorized
    return
}
// load your user from auth.UID if needed
user := LoadYourUser(auth.UID)
// or simply load your grants
perms := &Permission{}
err = auth.Load(perms);

In a single page application, you can regenerate a new access token by doing a GET request to /auth/refresh by default it has a cookie in there to give you an access token when you login. You'll need to also refresh it before it expires or just make it built-in to your http client.

Custom Emails

You can customize all emails by implementing the email interface you wish to change. You'll also need the email.Sender interface to actually be able to send emails.

// to enable email sending your provider must implement email.Sender interface.
func (ip *identityProvider) SendEmail(ctx context.Context, toEmail, subject, textBody, htmlBody string) error {
    // using smtp, sendgrid or any transactional email api here
	log.Println("ToEmail", toEmail, "\nSubject", subject, "\nTextBody: ", textBody)
	return nil
}

// for customizing email messages

// email.Confirmemail - for verifying the email
// email.UpdateEmail - when you are updating email
// email.ResetPassword - reset link
// email.LoginEmail - login link for passwordless login

func (ip *identityProvider) ConfirmEmail() (string, []email.Part) {
    return "Verify Email", []email.Part{
		{P: "Hi {name}"},
		{P: "Please click the link below to verify"},
		{URL: "{link}", Label: "Verify"},
	}
}

For the actual email template you'll need to update email.Template before you do any emailData.Parse.

Endpoints

Here are the default endpoints. You can change these in your config.

Register

URL : /auth/register

Method : POST or GET - provides the register page

Body

These are customizable fields under Fields. The IdentityFieldID is your email and PasswordFieldID for your password. This endpoint is not available if you are using passwordless system(PasswordFieldID is empty string). Here we have additional fields in your Fields for name and you provided Path.Terms so an Agree checkbox shows up in your registration and it's required to be true to successfull register.

{
    "email": "",
    "name": "",
    "password": "",
    "terms": true
}
Success Response

Code : 200 OK or 201 Created

201 means an email verification link has been sent, otherwise it's 200.

Error Response

Code : 400 Bad Request

Field: error message will be provided on any type of errors, depending on your provided validator you'll get the same message here.

{
    "error": "validation",
    "data": {
        "email": "required",
        "name": "required"
    }
}
Login

URL : /auth/login

Method : POST or GET - provides the Login Page

Body

Providing a PasswordFieldID will allow you to login with IdentityFieldID and .

{
    "email": "",
    "password": ""
}
Success Response

Code : 200 OK or 201 Created

201 means an email verification link has been sent(if it's passwordless then it's same here), otherwise it's 200. Also a refresh token would be written in body or if you provided RefreshTokenCookieName it will be written in cookie under /auth/refresh.

{
    "refresh_token": "..."
}
Error Response

Code : 400 Bad Request

Field: error message will be provided on any type of errors.

{
    "error": "validation",
    "data": {
        "password": "invalid"
    }
}
Access Token

URL : /auth/refresh

Method : POST or GET - when you have cookie enabled or DELETE - for logging out

Body

When doing a POST you need to pass in token with your refresh token to get a new access token.

{
    "token": "..."
}
Success Response

Code : 200 OK

You can now use your access_token in Authorization: Bearer ... for your authorized requests.

{
    "access_token": "...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "access"
}
Error Response

Code : 401 Unauthorized

{
    "error": "Unauthorized"
}
Account

URL : /auth/account

Method : POST or GET - provides the account page

Body

This is the same as the account page. But here you only send the fields you want updated, otherwise it would trigger the validation. Updating your EmailFieldID would trigger an email verification without updating current email. Clicking the email will then update your email if you are logged in.

{
    "name": "",
    // to enable 2FA (use action endpoint to create secure random secret and show QRCode image)
    "totpsecret": "secret",
    "code": "123123",
    // recoverycodes are 10 character alphanumeric separated by pipe | (use action endpoint below to generate secure random codes)
    "recoverycodes": "12345abcde|54321edcba|..."
}
Success Response

Code : 200 OK or 201 Created

201 means an email verification link has been sent, otherwise it's 200. Plus all the gauth.Fields

{
    "email": "email@example.com",
    "name": "The Name",
    "totpsecret": true // if 2fa is enabled otherwise it's not present
    "recoverycodes": 10 // recovery codes remaining when generated, otherwise it's not present
}
Error Response

Code : 400 Bad Request

Field: error message will be provided on any type of errors, depending on your provided validator you'll get the same message here.

{
    "error": "validation",
    "data": {
        "name": "required"
    }
}
Action

URL : /auth/action

Method : POST or GET

Body

Getting a token from different actions can be used here or triggers like reset link.

  • newRecovery - returns a list of 10 random recovery codes, save this under /auth/account field FieldRecoveryCodesID or gauth:"recoverycodes" tag.
  • newTotpKey - returns a secret and url for showing a qr code, save under /auth/account/ field FieldTOTPSecretID (or gauth:"totpsecret" tag) with code to enable 2FA.
  • verify - requires token body for verifying an email.
  • resetlink - requires IdentityFieldID for sending a reset link.
  • reset - requires PasswordFieldID and token for resetting password.
  • confirmemail - requires IdentityFieldID for resending verification link.
  • emailupdate - requires Authrozation header and token body.
{
    "action": "newRecovery|newTotpKey|verify|resetlink|reset|confirmemail|emailupdate",
    "token": ""
}
Success Response

Code : 200 OK

Error Response

Code : 400 Bad Request

Field: error message will be provided on any type of errors, depending on your provided validator you'll get the same message here.

{
    "error": "invalid action"
}

Documentation

Index

Constants

View Source
const (
	// AuthKey used to store value of *Auth in context
	AuthKey ctxKey = "authKey"
	// RequestKey for accessing request inside context
	RequestKey ctxKey = "requestKey"
)
View Source
const (
	FieldActiveID        = "active"
	FieldCodeID          = "code"
	FieldTOTPSecretID    = "totpsecret"
	FieldRecoveryCodesID = "recoverycodes"
	FieldRememberID      = "remember"
	FieldTermsID         = "terms"
)

Variables

View Source
var (
	ErrNoToken            = errors.New("no token")
	ErrInvalidAccessToken = errors.New("invalid access token")
)
View Source
var (
	ErrIdentityNotFound = errors.New("identity not found")
	// Return this error in IdentityLoad to provide Re-Send Activation link flow
	ErrIdentityNotActive = errors.New("identity not active")
	// Return in Token Providers to return 401 instead of 500
	ErrTokenDenied = errors.New("token denied")
)

Functions

func RequiredEmail

func RequiredEmail(fID string, data map[string]interface{}) error

func RequiredPassword

func RequiredPassword(fID string, data map[string]interface{}) error

func RequiredText

func RequiredText(fID string, data map[string]interface{}) error

Types

type AccessTokenProvider

type AccessTokenProvider interface {
	// Optionally implement this to add additional claims under "grants"
	// and add more role and access information for your token, this token is what's checked against
	// your middleware.
	CreateAccessToken(ctx context.Context, uid string, cid string) (interface{}, error)
}

type Auth

type Auth struct {
	UID    string          `json:"sub"`
	Grants json.RawMessage `json:"grants"`
}

func AuthFromContext

func AuthFromContext(ctx context.Context) *Auth

func (*Auth) Load

func (a *Auth) Load(dst interface{}) error

Load populates your Grants struct

type Claims

type Claims jwt.MapClaims

type DefaultAccessTokenProvider

type DefaultAccessTokenProvider struct {
	// contains filtered or unexported fields
}

func (*DefaultAccessTokenProvider) CreateAccessToken

func (da *DefaultAccessTokenProvider) CreateAccessToken(ctx context.Context, uid string, cid string) (interface{}, error)

Default behaviour of access token is check cid against client and current pw hash and "access" grants

type DefaultRefreshTokenProvider

type DefaultRefreshTokenProvider struct {
	// contains filtered or unexported fields
}

func (*DefaultRefreshTokenProvider) CreateRefreshToken

func (dr *DefaultRefreshTokenProvider) CreateRefreshToken(ctx context.Context, uid string) (cid string, err error)

Default behaviour of refresh token is using cid -> IP + UserAgent + PWHash

func (*DefaultRefreshTokenProvider) DeleteRefreshToken

func (dr *DefaultRefreshTokenProvider) DeleteRefreshToken(ctx context.Context, uid, cid string) error

Default behaviour of logout is in memory black list of cid that only keeps the last 500

type GAuth

type GAuth struct {
	// IdentityProvider must be implemented for saving your user and notifications
	IdentityProvider IdentityProvider

	// Fields for login/register/settings page fields
	Fields []*form.Field

	// Field for email verifications
	EmailFieldID string
	// Identity field is the field for logging in
	IdentityFieldID string
	// Leave blank to use email link for login
	PasswordFieldID string

	// Path for login, register, etc
	// defaults to /login /register /account /refresh
	Path   form.Path
	Logger *log.Logger

	// By default this uses embedded alpineJS
	AlpineJSURL string
	// Provide a secret to activate recaptcha in register
	RecaptchaSiteKey string
	RecaptchaSecret  string
	// JwtKey used for registration and token login
	JwtKey     []byte
	BCryptCost int

	// RefreshTokenCookieName defaults to rtoken with NewDefault(), set to blank to not set a cookie
	RefreshTokenCookieName string
	// AccessTokenCookieName default is blank, enable to set access token on /
	AccessTokenCookieName string

	// Page branding
	Brand form.Brand

	RateLimit RateLimit
	Timeout   Timeout

	// defaults to "gauth"
	StructTag string
	// contains filtered or unexported fields
}

GAuth is an HTTPServer which handles login, registration, settings, 2fa, etc.

func NewDefault

func NewDefault(appName string, appURL string, ip IdentityProvider) *GAuth

NewDefault returns a sane default for GAuth, you can override properties

func NewPasswordless

func NewPasswordless(appName string, appURL string, ap IdentityProvider) *GAuth

NewPasswordless returns a passwordless login settings

func (*GAuth) AuthMiddleware

func (ga *GAuth) AuthMiddleware(next http.Handler) http.Handler

func (*GAuth) Authorized

func (ga *GAuth) Authorized(r *http.Request) (*Auth, error)

func (*GAuth) CreateAccessToken

func (ga *GAuth) CreateAccessToken(ctx context.Context, sub string, grants interface{}, expiry time.Time) (string, error)

CreateAccessToken returns an access token

func (*GAuth) CreateRefreshToken

func (ga *GAuth) CreateRefreshToken(ctx context.Context, uid, cid string, expiry time.Time) (string, error)

CreateRefreshToken you can use this to create custom tokens such as for API keys or anything that has a longer expiration than provided configration.

func (*GAuth) MustInit

func (ga *GAuth) MustInit(debug bool) *GAuth

func (*GAuth) ServeHTTP

func (ga *GAuth) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Identity

type Identity interface {
	// IdentitySave is called to safely save an account, fields provided with "gauth" tag will
	// automatically be updated with it's corresponding values based on registration/login/account
	// forms. Return the unique identifier of this account once saved.
	IdentitySave(ctx context.Context) (uid string, err error)
}

type IdentityProvider

type IdentityProvider interface {
	// IdentityUID should return a unique identifier from your Identifier field(email/username)
	// this will be use as the subject in your refresh and access token, you should return
	// ErrIdentityNotFound if it doesn't exists or ErrIdentityNotActive if they are not allowed to login while inactive.
	IdentityUID(ctx context.Context, id string) (uid string, err error)
	// IdentityLoad must return a struct that implements Identity interface, provide "gauth" tag
	// to map gauth.Fields ID to your struct properties. If the account does not exists you must
	// return an zero/default struct Identity that will be populated for a new registration.
	IdentityLoad(ctx context.Context, uid string) (identity Identity, err error)
}

IdentityProvider must be implemented to login, register, update your user/account.

type RateLimit

type RateLimit struct {
	Login        cache.Rate
	Register     cache.Rate
	ResetLink    cache.Rate
	ConfirmEmail cache.Rate
}

type RefreshTokenProvider

type RefreshTokenProvider interface {
	CreateRefreshToken(ctx context.Context, uid string) (cid string, err error)
	// Called on logout
	DeleteRefreshToken(ctx context.Context, uid, cid string) error
}

Optionally implement this interface to customize your refresh token with a specific client ID or anything that can be identified that is linked to the UID so you can easily revoke it somewhere.

type Timeout

type Timeout struct {
	// 7 days default
	EmailToken time.Duration
	// 1 day default
	RefreshToken time.Duration
	// 7 days default
	RefreshTokenRemember time.Duration
	// 1 hour default
	AccessToken time.Duration
}

type ValidationError

type ValidationError struct {
	Field   string
	Message string
}

func (ValidationError) Error

func (ve ValidationError) Error() string

Directories

Path Synopsis
cmd
Code generated by go generate; DO NOT EDIT.
Code generated by go generate; DO NOT EDIT.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL