omniauth

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

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

go-ruby-omniauth/omniauth

omniauth — go-ruby-omniauth

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the engine of Ruby's OmniAuth — the Rack middleware and strategy framework that standardises multi-provider authentication — matching the observable behaviour of the omniauth gem (OmniAuth 2.x, MRI 4.0.5). It owns the request/callback routing state-machine over /auth/:provider and /auth/:provider/callback, the allowed-request-methods gate, the request-phase CSRF hook, the AuthHash shape and the failure-redirect flow — without any Ruby runtime.

It is the OmniAuth backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-rack (whose Rack Env, Request, Response and Headers it reuses) and go-ruby-erb.

The engine, not the providers. OmniAuth's value is the framework: routing, phases, the auth-hash contract, failure handling. That is exactly what this package models. The provider-specific bodies (what a request phase redirects to, how a callback resolves an identity) and the HTTP session are seams — the host supplies them, so the core stays pure and dependency-light. A later go-embedded-ruby binding plugs the Ruby strategy objects into these seams.

The StrategyPhase seam

Every provider difference funnels through one function:

type StrategyPhase func(name, phase string, env any) PhaseResult

The engine calls it with the provider name, the phase (omniauth.PhaseRequest = "request" or omniauth.PhaseCallback = "callback") and the Rack env. For the request phase it returns a PhaseResult{Response: …} — typically a 302 to the provider. For the callback phase it returns a PhaseResult{Auth: …} — the resolved [AuthHash], which the engine stores at env["omniauth.auth"] before calling through to the app. Either phase may instead return PhaseResult{Fail: "reason", Err: …} to enter the failure flow. The provider logic itself (OAuth handshakes, token exchange) lives in the seam, outside this package.

Install

go get github.com/go-ruby-omniauth/omniauth

Usage

package main

import (
	"github.com/go-ruby-omniauth/omniauth"
	"github.com/go-ruby-rack/rack"
)

func main() {
	cfg := omniauth.DefaultConfig() // "/auth" prefix, POST-only, session required

	// Register providers. A real provider does an OAuth redirect / token
	// exchange here; this fake one shows the two phases.
	strategies := omniauth.NewStrategies()
	strategies.Register("developer", func(name, phase string, env any) omniauth.PhaseResult {
		if phase == omniauth.PhaseRequest {
			r := omniauth.Response{ /* 302 to the provider */ }
			return omniauth.PhaseResult{Response: &r}
		}
		// callback: resolve the identity
		auth := omniauth.NewAuthHash().SetUID("12345")
		auth.Info().Set("email", "ada@example.com")
		return omniauth.PhaseResult{Auth: auth}
	})

	// The app that runs once authentication succeeds.
	app := omniauth.AppFunc(func(env rack.Env) (omniauth.Response, error) {
		_ = env["omniauth.auth"].(*omniauth.AuthHash) // provider, uid, info, …
		return omniauth.Response{Status: 200, Headers: rack.NewHeaders(), Body: []string{"signed in"}}, nil
	})

	// Mount the providers as middleware over the app.
	handler, _ := omniauth.NewBuilder(cfg, strategies).
		Provider("developer", nil).
		Build(app)

	_ = handler // handler.Call(env) drives /auth/developer and /auth/developer/callback
}

API

// engine configuration
func DefaultConfig() *Config
type Config struct { PathPrefix; AllowedRequestMethods; RequireSession; TestMode;
                     MockAuth; MockFailure; FailureRaiseOut; RequestValidationPhase;
                     BeforeRequestPhase; BeforeCallbackPhase; OnFailure; Logger }
func AuthenticityTokenProtection(valid func(env any) bool) func(env any) error

// provider registry + middleware stack
type Strategies struct{ … }
func NewStrategies() *Strategies
func (r *Strategies) Register(name string, phase StrategyPhase) *Strategies
func (r *Strategies) Lookup(name string) (StrategyPhase, bool)
func (r *Strategies) Names() []string

type Builder struct{ … }
func NewBuilder(config *Config, strategies *Strategies) *Builder
func (b *Builder) Provider(name string, options *Options) *Builder
func (b *Builder) Build(app App) (App, error)
func PassThroughApp() App

// the middleware / phase state-machine
type Strategy struct{ … }
func NewStrategy(name string, app App, phase StrategyPhase, config *Config, options *Options) *Strategy
func (s *Strategy) Call(env rack.Env) (Response, error) // OmniAuth::Strategy#call!
func (s *Strategy) Name() string
func (s *Strategy) Options() *Options

// the provider seam
type StrategyPhase func(name, phase string, env any) PhaseResult
type PhaseResult struct { Response *Response; Auth *AuthHash; Fail string; Err error }
const PhaseRequest, PhaseCallback = "request", "callback"

// the auth hash (OmniAuth::AuthHash)
type AuthHash struct{ … }
func NewAuthHash() *AuthHash
func AuthHashOf(pairs ...any) *AuthHash
func (h *AuthHash) Set(key string, val any) *AuthHash
func (h *AuthHash) Get / GetOK / Has / Delete / Keys / Len / GetString
func (h *AuthHash) Provider() string
func (h *AuthHash) UID() string
func (h *AuthHash) SetUID(uid string) *AuthHash
func (h *AuthHash) ValidQ() bool                 // valid? (provider && uid)
func (h *AuthHash) Info() *InfoHash              // info sub-hash
func (h *AuthHash) Credentials() *AuthHash
func (h *AuthHash) Extra() *AuthHash
type InfoHash struct{ *AuthHash }
func (i *InfoHash) Name() string                 // name || "first last" || nickname || email

// responses, errors, failure
type Response struct { Status int; Headers *rack.Headers; Body []string }
type App interface{ Call(env rack.Env) (Response, error) }
type AppFunc func(env rack.Env) (Response, error)
type Error struct { MessageKey, Msg string; Err error }
type AuthenticityError struct{ … }               // request-phase CSRF failure
type NoSessionError struct{ … }                  // no rack.session
type FailureEndpoint struct{ … }                 // default on_failure

Fidelity vs the omniauth gem (2.x)

OmniAuth (Ruby) This package
OmniAuth::Builder / provider :name Builder + Strategies.Register / Builder.Provider
OmniAuth::Strategy#call! routing Strategy.Call — request / callback / options / pass-through
path_prefix, request_path, callback_path Config.PathPrefix, Options.RequestPath / CallbackPath
allowed_request_methods (default [:post]) Config.AllowedRequestMethods (default POST)
request_validation_phase (AuthenticityToken) Config.RequestValidationPhase + AuthenticityTokenProtection
request_phase → provider redirect StrategyPhase(PhaseRequest)PhaseResult.Response
callback_phaseenv["omniauth.auth"] StrategyPhase(PhaseCallback)PhaseResult.Auth
OmniAuth::AuthHash (info/credentials/extra) AuthHash + InfoHash (indifferent-access, #name derivation)
fail!on_failure/auth/failure?… Strategy.failConfig.OnFailureFailureEndpoint
OmniAuth::Error / AuthenticityError / NoSessionError same three types
test mode + mock_auth Config.TestMode + MockAuth / MockFailure

Out of scope (host/seam concerns): the socket accept loop and Rack handler (go-ruby-rack's doc.go notes these belong to the host), the HTTP session store, the concrete provider strategies (OAuth1/OAuth2/OpenID handshakes), and CSRF token storage — the RequestValidationPhase wires the raise/allow decision, the host supplies the token check. The Rack Env/Request/Response/Headers are reused directly from go-ruby-rack.

Tests & coverage

The suite is deterministic and Ruby-free: the engine — request/callback routing, the allowed-methods gate, the CSRF hook, the success auth-hash, the failure redirect and the test-mode mock provider — is exercised through fake strategy and env seams. Those tests alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

CGO-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — including big-endian s390x) and three OSes (Linux, macOS, Windows).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-omniauth/omniauth authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package omniauth is a pure-Go (no cgo) reimplementation of the engine of Ruby's OmniAuth — the Rack middleware and strategy framework that standardises multi-provider authentication — matching the observable behaviour of the `omniauth` gem (OmniAuth 2.x, MRI 4.0.5).

It models the parts of OmniAuth that are interpreter-independent and provider -agnostic: the request/callback routing state-machine over the `/auth/:provider` and `/auth/:provider/callback` paths, the allowed-request-methods gate, the request-phase CSRF (AuthenticityTokenProtection) hook, the shape of the AuthHash (provider/uid/info/credentials/extra), and the failure flow that redirects to `/auth/failure?message=…&strategy=…`. The provider-specific bodies — what a request phase redirects to, what a callback phase resolves the identity to — and the HTTP session are host concerns, supplied through the StrategyPhase seam and the Rack env.

The Rack environment, request and response types come from github.com/go-ruby-rack/rack, so this package composes with the rest of the go-ruby-* Rack stack without a second Rack model. The package is the OmniAuth backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-rack and go-ruby-erb.

Index

Constants

View Source
const (
	// PhaseRequest is the request phase: kick off authentication, typically by
	// redirecting the browser to the provider.
	PhaseRequest = "request"
	// PhaseCallback is the callback phase: the provider has returned, resolve the
	// authenticated identity into an [AuthHash].
	PhaseCallback = "callback"
)

Phase names passed to a StrategyPhase.

Variables

This section is empty.

Functions

func AuthenticityTokenProtection

func AuthenticityTokenProtection(valid func(env any) bool) func(env any) error

AuthenticityTokenProtection builds a request_validation_phase hook that fails with an AuthenticityError unless valid reports the request's CSRF token good. The token check itself (session vs request param) is the host's, kept out of this pure-Go core; this wires the raise/allow decision faithfully.

Types

type App

type App interface {
	Call(env rack.Env) (Response, error)
}

App is a Rack application: it receives an env and returns a response, or an error to propagate (a raised exception in Rack terms). Each Strategy is an App that wraps the downstream App.

func PassThroughApp

func PassThroughApp() App

PassThroughApp returns a terminal 404 app, handy as the base of a Build when the middleware stack is meant to fully own the request.

type AppFunc

type AppFunc func(env rack.Env) (Response, error)

AppFunc adapts a function to the App interface.

func (AppFunc) Call

func (f AppFunc) Call(env rack.Env) (Response, error)

Call implements App.

type AuthHash

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

AuthHash models OmniAuth::AuthHash: the indifferent-access hash that a strategy's callback phase builds and exposes at env["omniauth.auth"]. Ruby's AuthHash keys symbols and strings the same; here keys are plain strings, so h.Get("uid") and h.Get(:uid) collapse to the one lookup. A canonical auth hash carries the top-level "provider" and "uid" plus the "info", "credentials" and "extra" sub-hashes. Keys iterate in first-insertion order, matching Ruby Hash.

func AuthHashOf

func AuthHashOf(pairs ...any) *AuthHash

AuthHashOf builds an AuthHash from pairs, applied in order via Set.

func NewAuthHash

func NewAuthHash() *AuthHash

NewAuthHash returns an empty AuthHash.

func (*AuthHash) Credentials

func (h *AuthHash) Credentials() *AuthHash

Credentials returns the "credentials" sub-hash, creating it on first access.

func (*AuthHash) Delete

func (h *AuthHash) Delete(key string) (any, bool)

Delete removes key, returning its prior value and whether it was present.

func (*AuthHash) Extra

func (h *AuthHash) Extra() *AuthHash

Extra returns the "extra" sub-hash, creating it on first access.

func (*AuthHash) Get

func (h *AuthHash) Get(key string) any

Get returns the value for key, or nil if absent.

func (*AuthHash) GetOK

func (h *AuthHash) GetOK(key string) (any, bool)

GetOK returns the value for key and whether it was present.

func (*AuthHash) GetString

func (h *AuthHash) GetString(key string) string

GetString returns the string value for key, or "" if absent or non-string.

func (*AuthHash) Has

func (h *AuthHash) Has(key string) bool

Has reports whether key is present.

func (*AuthHash) Info

func (h *AuthHash) Info() *InfoHash

Info returns the "info" sub-hash as an InfoHash, creating it (and adopting a plain sub-hash already stored there) on first access, like AuthHash#info.

func (*AuthHash) Keys

func (h *AuthHash) Keys() []string

Keys returns the keys in first-insertion order.

func (*AuthHash) Len

func (h *AuthHash) Len() int

Len reports the number of keys.

func (*AuthHash) Provider

func (h *AuthHash) Provider() string

Provider returns the "provider" value as a string.

func (*AuthHash) Set

func (h *AuthHash) Set(key string, val any) *AuthHash

Set assigns key to val (insertion-ordered) and returns h for chaining.

func (*AuthHash) SetUID

func (h *AuthHash) SetUID(uid string) *AuthHash

SetUID sets the "uid" value and returns h.

func (*AuthHash) UID

func (h *AuthHash) UID() string

UID returns the "uid" value as a string.

func (*AuthHash) ValidQ

func (h *AuthHash) ValidQ() bool

ValidQ reports whether the hash has both a provider and a uid, matching AuthHash#valid? (a hash is valid once it identifies who authenticated where).

type AuthenticityError

type AuthenticityError struct {
	Base *Error
}

AuthenticityError is raised by the request-validation (CSRF) hook when the authenticity token is missing or wrong — OmniAuth::AuthenticityError. It maps to the "authenticity_error" failure key.

func NewAuthenticityError

func NewAuthenticityError(msg string) *AuthenticityError

NewAuthenticityError builds an AuthenticityError with the given message.

func (*AuthenticityError) Error

func (e *AuthenticityError) Error() string

Error implements the error interface.

func (*AuthenticityError) MessageKeyValue

func (e *AuthenticityError) MessageKeyValue() string

MessageKeyValue returns the failure message key.

func (*AuthenticityError) Unwrap

func (e *AuthenticityError) Unwrap() error

Unwrap exposes the base error.

type Builder

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

Builder mounts providers as a stack of Strategy middlewares over an app, mirroring OmniAuth::Builder (a Rack::Builder that adds a `provider` DSL).

func NewBuilder

func NewBuilder(config *Config, strategies *Strategies) *Builder

NewBuilder returns a Builder that resolves providers through strategies under config. Both must be non-nil.

func (*Builder) Build

func (b *Builder) Build(app App) (App, error)

Build wraps app with the mounted providers, outermost first (the first provider mounted sees the request first, as with Rack `use` ordering). It fails if a mounted provider was never registered in the Strategies.

func (*Builder) Provider

func (b *Builder) Provider(name string, options *Options) *Builder

Provider mounts the named provider with the given options (nil is fine) and returns the Builder for chaining — the analogue of `provider :name, …`.

type Config

type Config struct {
	// PathPrefix is the mount point for the auth routes (default "/auth"), so the
	// request path is "<PathPrefix>/<provider>" and the callback is
	// "<PathPrefix>/<provider>/callback".
	PathPrefix string

	// AllowedRequestMethods lists the upper-case HTTP verbs permitted on the
	// request path. Empty means the OmniAuth 2.x default, POST only.
	AllowedRequestMethods []string

	// RequireSession makes the middleware raise a [NoSessionError] when a request
	// arrives without a Rack session (env["rack.session"]). Default true.
	RequireSession bool

	// TestMode short-circuits the strategies to the mock flow: the request phase
	// redirects straight to the callback and the callback phase serves a mocked
	// auth hash from MockAuth / MockFailure instead of talking to a provider.
	TestMode bool

	// MockAuth maps a provider name (or "default") to the auth hash its mocked
	// callback yields in TestMode.
	MockAuth map[string]*AuthHash

	// MockFailure maps a provider name to a failure key; a mocked callback for
	// such a provider takes the failure flow in TestMode.
	MockFailure map[string]string

	// FailureRaiseOut makes the default failure endpoint propagate the error
	// instead of redirecting — OmniAuth's raise_out! for dev/test environments.
	FailureRaiseOut bool

	// RequestValidationPhase is the request-phase CSRF hook
	// (OmniAuth's request_validation_phase / AuthenticityTokenProtection). A
	// non-nil error it returns takes the failure flow. nil disables the check.
	RequestValidationPhase func(env any) error

	// BeforeRequestPhase and BeforeCallbackPhase run just before the respective
	// phase, mirroring OmniAuth.config.before_request_phase / before_callback_phase.
	BeforeRequestPhase  func(env any)
	BeforeCallbackPhase func(env any)

	// OnFailure resolves a failure into a response. fail! sets
	// env["omniauth.error"], ["omniauth.error.type"] and ["omniauth.error.strategy"]
	// before calling it. Default is the [FailureEndpoint] redirect.
	OnFailure func(env rack.Env) (Response, error)

	// Logger receives (level, message) log lines; nil discards them.
	Logger func(level, message string)
}

Config holds the engine-wide settings, the analogue of OmniAuth.config. The zero value is not usable; build one with DefaultConfig and adjust.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a Config with OmniAuth 2.x defaults: "/auth" prefix, POST-only request methods, session required, the failure redirect wired up.

type Error

type Error struct {
	// MessageKey is the symbolic failure reason (e.g. "invalid_credentials").
	MessageKey string
	// Msg is the human-readable message.
	Msg string
	// Err is an optional wrapped cause.
	Err error
}

Error is the base OmniAuth error (OmniAuth::Error). It carries the failure message key that the failure flow reflects into `/auth/failure?message=<key>` — the machine-readable reason, distinct from the human message.

func NewError

func NewError(key, msg string) *Error

NewError builds an Error with the given key and message.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) MessageKeyValue

func (e *Error) MessageKeyValue() string

MessageKeyValue returns the failure message key. It is promoted to the specific error types below, so the engine can extract a key from any OmniAuth error uniformly.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the wrapped cause, if any.

type FailureEndpoint

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

FailureEndpoint is the default on_failure handler — OmniAuth::FailureEndpoint. It reads the failure context that fail! stored in the env and either propagates the error (raise_out!) or redirects the browser to the failure path `<prefix>/failure?message=<key>&strategy=<name>`.

func NewFailureEndpoint

func NewFailureEndpoint(env rack.Env, config *Config) *FailureEndpoint

NewFailureEndpoint wraps env and config for a single failure resolution.

func (*FailureEndpoint) Call

func (f *FailureEndpoint) Call() (Response, error)

Call resolves the failure: it raises out (returns the stored error) when FailureRaiseOut is set, otherwise it redirects to the failure path.

type InfoHash

type InfoHash struct {
	*AuthHash
}

InfoHash models OmniAuth::AuthHash::InfoHash — the "info" sub-hash whose #name is derived from the other name-like fields when absent.

func NewInfoHash

func NewInfoHash() *InfoHash

NewInfoHash returns an empty InfoHash.

func (*InfoHash) Name

func (i *InfoHash) Name() string

Name returns the display name, matching InfoHash#name: an explicit "name", else "first_name last_name" (either part alone is allowed), else "nickname", else "email", else "".

type NoSessionError

type NoSessionError struct {
	Base *Error
}

NoSessionError is raised when a request reaches the middleware without a Rack session — OmniAuth::NoSessionError. Unlike a strategy failure it is not redirected; it propagates to the host, which must mount a session store ahead of OmniAuth.

func NewNoSessionError

func NewNoSessionError(msg string) *NoSessionError

NewNoSessionError builds a NoSessionError with the given message.

func (*NoSessionError) Error

func (e *NoSessionError) Error() string

Error implements the error interface.

func (*NoSessionError) MessageKeyValue

func (e *NoSessionError) MessageKeyValue() string

MessageKeyValue returns the failure message key.

func (*NoSessionError) Unwrap

func (e *NoSessionError) Unwrap() error

Unwrap exposes the base error.

type Options

type Options struct {
	// PathPrefix overrides Config.PathPrefix for this strategy when non-empty.
	PathPrefix string
	// RequestPath overrides the derived request path when non-empty.
	RequestPath string
	// CallbackPath overrides the derived callback path when non-empty.
	CallbackPath string
	// Args carries arbitrary provider options (client id/secret, scope, …).
	Args map[string]any
}

Options are per-strategy settings, the analogue of a strategy's options block. A zero Options is fine; unset fields fall back to the engine Config.

type PhaseResult

type PhaseResult struct {
	// Response short-circuits the phase with this response.
	Response *Response
	// Auth is the callback-phase resolved identity.
	Auth *AuthHash
	// Fail, when non-empty, is the failure message key to fail with.
	Fail string
	// Err is the error carried into the failure env alongside Fail.
	Err error
}

PhaseResult is what a StrategyPhase returns to the engine.

For PhaseRequest, set Response to the provider redirect (or any short-circuit response). For PhaseCallback, set Auth to the resolved identity — the engine stores it at env["omniauth.auth"] and calls through to the app — or set Response to short-circuit. Either phase may instead set Fail (with an optional Err) to take the failure flow.

type Response

type Response struct {
	Status  int
	Headers *rack.Headers
	Body    []string
}

Response is a Rack response tuple `[status, headers, body]`, the value a Rack application (and thus each middleware phase) returns. Headers reuses rack.Headers so it composes with the rest of the go-ruby-* Rack stack.

type Strategies

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

Strategies is the provider registry — the analogue of the OmniAuth::Strategies namespace. A provider registers its StrategyPhase under a name, and a Builder resolves each mounted provider through it. This decouples the set of available providers from the middleware stack.

func NewStrategies

func NewStrategies() *Strategies

NewStrategies returns an empty registry.

func (*Strategies) Lookup

func (r *Strategies) Lookup(name string) (StrategyPhase, bool)

Lookup returns the phase handler registered for name.

func (*Strategies) Names

func (r *Strategies) Names() []string

Names returns the registered provider names in registration order.

func (*Strategies) Register

func (r *Strategies) Register(name string, phase StrategyPhase) *Strategies

Register adds (or replaces) the phase handler for a provider name and returns the registry for chaining.

type Strategy

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

Strategy is one provider's middleware instance: it wraps the downstream App, intercepts that provider's request and callback paths, and passes everything else through. It is OmniAuth::Strategy — the unit `Builder#provider` mounts.

func NewStrategy

func NewStrategy(name string, app App, phase StrategyPhase, config *Config, options *Options) *Strategy

NewStrategy builds a Strategy for provider name that wraps app, running phase for its request/callback bodies under config. options may be nil.

func (*Strategy) Call

func (s *Strategy) Call(env rack.Env) (Response, error)

Call is the middleware entry point (OmniAuth::Strategy#call!). It routes the request to the request or callback phase when the path and method match, runs the mock flow in test mode, and otherwise passes through to the wrapped app.

func (*Strategy) Name

func (s *Strategy) Name() string

Name returns the provider name.

func (*Strategy) Options

func (s *Strategy) Options() *Options

Options returns the strategy options (never nil).

type StrategyPhase

type StrategyPhase func(name, phase string, env any) PhaseResult

StrategyPhase is the provider seam: the engine calls it with the provider name, the phase ("request" or "callback") and the Rack env (typed `any` so the seam is free of this package's Rack choice), and it returns a PhaseResult. This is where provider-specific logic — OAuth redirects, token exchange, identity mapping — lives, outside this pure engine.

Jump to

Keyboard shortcuts

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