wru

package module
v0.0.0-...-2e66719 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2021 License: Apache-2.0 Imports: 43 Imported by: 0

README

WRU: who are you

wru logo

WRU is an identity aware reverse proxy/middleware for enterprise users. It provides seamless authentication experience between development environemnt and production environment.

What is WRU for

  • For enterprise users

    • Easy to inject users via CSV files on storage (local, AWS S3, GCP Cloud Storage)
  • Not For consumer service users

    • It is not supporting creating user
  • For testing

    • Inject user information from env vars/files. You can setup via Docker easily
    • No password required (E2E test friendly)
  • For production

    • It supports OpenID Connect, some SNS (Twitter and GitHub for now) to login.

Use as Reverse Proxy

Getting Started

You can get wru by "go get".

$ go get -u	github.com/future-architect/future-wru/cmd/wru
$ wru
Port: 8000
TLS: enabled
Debug: false
Forward To:
  / => http://localhost:8080 ()
Twitter Login: OK
GitHub Login: OK
Users (for Debug):
  (User) 'test user 1'(user1) @ R&D (scopes: admin, user, org:rd)
  (User) 'test user 2'(user2) @ HR (scopes: user, org:hr)
starting wru server at https://localhost:8000

wru command doesn't have command line options. You can control it via environment variables.

WRU modes

WRU has two modes. This absorbs the difference of usecases and your web service always get only authorized requests.

WRU for local development

dev-mode

Sample configuration:

$ export WRU_DEV_MODE=true
$ export WRU_TLS_CERT="-----BEGIN CERTIFICATE-----\naaaabbbbbcccccdddd....zzzz\n-----END CERTIFICATE-----"
$ export WRU_TLS_KEY="-----BEGIN PRIVATE KEY-----\nZZZZYYYYYYXXXX.....BBBBAAAA\n-----END PRIVATE KEY-----"
$ export WRU_FORWARD_TO="/ => http://localhost:8080"
$ export WRU_USER_1="id:user1,name:test user 1,mail:user1@example.com,org:R&D,scope:admin,scope:user,scope:org:rd,twitter:user1,github:user1"
$ export WRU_USER_2="id:user2,name:test user 2,mail:user2@example.com,org:HR,scope:user,scope:org:hr,twitter:user2,github:user2"
$ PORT=8000 HOST=https://localhost:8000 wru
WRU for production

production-mode

Sample configuration:

  • Launch at example.com (local port is 8000)
  • No HTTPS by wru (AWS ALB does)
  • A backend server is at http://server.example.com
  • User information is in S3 (and reread it every hour)
  • Session storage is in DynamoDB
  • Twitter/GitHub/OpenID Connect login is available
$ export WRU_DEV_MODE=false
$ export WRU_FORWARD_TO="/ => http://server.example.com"
$ export WRU_USER_TABLE="s3://my-app-usertable/user-list.csv?region=us-west-1"
$ export WRU_USER_TABLE_RELOAD_TERM=1h
$ export WRU_TWITTER_CONSUMER_KEY=1111111
$ export WRU_TWITTER_CONSUMER_SECRET=22222222
$ export WRU_GITHUB_CLIENT_ID=33333333
$ export WRU_GITHUB_CLIENT_SECRET=44444444
$ export WRU_OIDC_PROVIDER_URL=http://keycloak.example.com
$ export WRU_OIDC_CLIENT_ID=55555555
$ export WRU_OIDC_CLIENT_SECRET=66666666
$ PORT=8000 HOST=https://example.com wru
End Points for frontend
  • /.wru/login: Login page
  • /.wru/logout: Logout page (it works just GET access)
  • /.wru/user: User page (it supports HTML and JSON)
  • /.wru/user/sessions: User session page (it supports HTML and JSON)
Session Storage

It supports session storage feature similar to browsers' cookie.

session storage

Your web application sends data that will be in the session storage with in Wru-Set-Session-Data header field in response like this:

Wru-Set-Session-Data: access-count=10

wru filter this content (browser doesn't retrieve this header field) and store its content in session storage. This content is added to Wru-Session header field (you can modify via WRU_SERVER_SESSION_FIELD env var) like this:

Wru-Session: {"login_at":1212121,"id":"shibu","name":"Yoshiki Shibukawa","scopes":["user","admin"],data:{"access-count":"10"}}

To read all content of this field in Go, you can parse it via the following structure:

type Session struct {
	LoginAt      int64             `json:"login_at"`       // nano-seconds
	ExpireAt     int64             `json:"expire_at"`      // nano-seconds
	LastAccessAt int64             `json:"last_access_at"` // nano-seconds
	UserID       string            `json:"id"`
	DisplayName  string            `json:"name"`
	Email        string            `json:"email"`
	Organization string            `json:"org"`
	Scopes       []string          `json:"scopes"`
	Data         map[string]string `json:"data"`
}

func ParseSession(r *http.Request) (*Session, error) {
  h := r.Header.Get("Wru-Session")
  if h != "" {
    var s Session
    err := json.NewDecoder(strings.NewReader(h)).Decode(&s)
    if err != nil {
      return nil, err
    }
    return &s, nil
  }
  return nil, err
}
Configuration
Server Configuration
  • PORT: Port number that wru uses (default is 3000)
  • HOST: Host name that wru is avaialble (required). It is used for callback of OAuth/OpenID Connect.
  • WRU_DEV_MODE: Change mode (described bellow)
  • WRU_TLS_CERT and WRU_TLS_KEY: Launch TLS server
Storage Configuration

WRU stores user information on-memory. You can add user via CSV or env vars.

  • WRU_SESSION_STORAGE: Session storage. Default is in memory. It supports DynamoDB, Firestore, MongoDB.
  • WRU_USER_TABLE: This is local file path/Blob path(AWS S3, GCP Cloud Storage) to read CSV.
  • WRU_USER_TABLE_RELOAD_TERM: Reload term.
  • WRU_USER_%d: Add user via environment variable (for testing).

If you add user via env var, you use comma separated tag list:

WRU_USER_1="id:user1,name:test user 1,mail:user1@example.com,org:R&D,scope:admin,scope:user,scope:org:rd,twitter:user1"

User table CSV file should have specific header row.

id,name,mail,org,scopes,twitter,github,oidc
user1,test user,user1@example.com,R&D,"admin,user,org:rd",user1,user1,user1@example.com
Backend Server Configuration
  • WRU_FORWARD_TO: Specify you backend server (required)
  • WRU_SERVER_SESSION_FIELD: Header field name that WRU adds to backend request (default is "Wru-Session")
Frontend User Experience Configuration
  • WRU_DEFAULT_LANDING_PAGE: WRU tries to redirect to referrer page after login. It is used when the path is not available (default is '/').
  • WRU_LOGIN_TIMEOUT_TERM: Login session token's expiration term (default is '10m')
  • WRU_SESSION_IDLE_TIMEOUT_TERM: Active session token's timeout term (default is '1h')
  • WRU_SESSION_ABSOLUTE_TIMEOUT_TERM: Absolute session token's timeout term (default is '720h')
  • WRU_HTML_TEMPLATE_FOLDER: Login/User pages' template (default tempalte is embedded ones)
ID Provider Configuration

To enable ID provider connection, set the following env vars. The callback address will be ${HOST}/.wru/callback. You should register the URL in the setting screen of the ID provider.

Twitter
  • WRU_TWITTER_CONSUMER_KEY
  • WRU_TWITTER_CONSUMER_SECRET
GitHub
  • WRU_GITHUB_CLIENT_ID
  • WRU_GITHUB_CLIENT_SECRET
OpenID Connect
  • WRU_OIDC_PROVIDER_URL
  • WRU_OIDC_CLIENT_ID
  • WRU_OIDC_CLIENT_SECRET
Extra Option
  • WRU_GEIIP_DATABASE: GeoIP2 or GeoLite2 file (.mmdb) to detect user location from IP address

Use as Middleware

wru can work as middleware of HTTP service. Sample is in cmd/sampleapp.

NewAuthorizationMiddleware() returns required HTTP handler (that includes, login form, callback for OAuth2 and so on) and middleware. Don't apply the middleware to the wru's handler (it causes infinity loop).

You can create *wru.Config by using the structure directly or wru.NewConfigFromEnv().

package main

import (
  "context"
  "fmt"
  "net/http"
  "os"
  "os/signal"

  "github.com/go-chi/chi/v5"
  "github.com/go-chi/chi/v5/middleware"
  "gitlab.com/osaki-lab/wru"

  // Select backend of session storage (docstore) and identity register (blob)
  _ "gocloud.dev/docstore/memdocstore"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	r := chi.NewRouter()
	c := &wru.Config{
		Port:    3000,
		Host:    "http://localhost:3000",
		DevMode: true,
		Users: []*wru.User{
			{
				DisplayName:  "Test User 1",
				Organization: "Secret",
				UserID:       "testuser01",
				Email:        "testuser01@example.com",
			},
		},
	}
	wruHandler, authMiddleware := wru.NewAuthorizationMiddleware(ctx, c, os.Stdout)
	r.Use(middleware.Logger)
	r.Mount("/", wruHandler)
	r.With(authMiddleware).Get("/", func(w http.ResponseWriter, r *http.Request) {
		_, session := wru.GetSession(r)
		w.Write([]byte("welcome " + session.UserID))
	})
    http.ListenAndServe(fmt.Sprintf(":%d", c.Port), r)
}

License

Apache 2

Documentation

Index

Constants

View Source
const (
	LoginPageTemplate        = "login.html"
	DebugLoginPageTemplate   = "debug_login.html"
	UserStatusPageTemplate   = "user_status.html"
	UserSessionsPageTemplate = "user_sessions.html"
)

Variables

View Source
var (
	ErrUserNotFound = errors.New("user not found")
	ErrNotModified  = errors.New("not modified")
)
View Source
var ErrInvalidSessionToken = errors.New("invalid session token")

Functions

func MustLogin

func MustLogin(c *Config, s SessionStorage) func(http.Handler) http.Handler

func MustNotLogin

func MustNotLogin(c *Config, s SessionStorage) func(http.Handler) http.Handler

func NewAuthorizationMiddleware

func NewAuthorizationMiddleware(ctx context.Context, c *Config, out io.Writer) (http.Handler, func(http.Handler) http.Handler)

func NewIdentityAwareProxyHandler

func NewIdentityAwareProxyHandler(c *Config, s SessionStorage, u *IdentityRegister) (http.Handler, error)

func NewReverseProxy

func NewReverseProxy(config *Config, s SessionStorage) (http.Handler, error)

func SplitBlobPath

func SplitBlobPath(resourceUrl string) (string, string, error)

Types

type AllUserSessions

type AllUserSessions []SingleSessionData

func (AllUserSessions) WriteAsJson

func (as AllUserSessions) WriteAsJson(w io.Writer) error

type ClientSessionFieldType

type ClientSessionFieldType int
const (
	CookieField ClientSessionFieldType = iota + 1
	CookieWithJSField
	InvalidField
)

type Config

type Config struct {
	Port uint16
	Host string

	DevMode bool

	AdminPort                uint16
	TlsCert                  string
	TlsKey                   string
	ForwardTo                []Route
	DefaultLandingPage       string
	UserTable                string
	UserTableReloadTerm      time.Duration
	SessionStorage           string
	ServerSessionField       string
	ClientSessionFieldCookie ClientSessionFieldType
	ClientSessionKey         string

	LoginTimeoutTerm           time.Duration
	SessionIdleTimeoutTerm     time.Duration
	SessionAbsoluteTimeoutTerm time.Duration

	HTMLTemplateFolder string

	Twitter TwitterConfig
	GitHub  GitHubConfig
	OIDC    OIDCConfig

	RedisSession RedisConfig

	GeoIPDatabasePath string

	Users []*User
	// contains filtered or unexported fields
}

func NewConfigFromEnv

func NewConfigFromEnv(ctx context.Context, out io.Writer) (*Config, error)

func (*Config) Init

func (c *Config) Init(ctx context.Context, out io.Writer) error

type Directive

type Directive struct {
	Key   string
	Value string
}

type FederatedAccount

type FederatedAccount struct {
	Service IDPlatform `json:"service"`
	Account string     `json:"account"`
}

type GitHubConfig

type GitHubConfig struct {
	ClientID     string
	ClientSecret string
}

func (GitHubConfig) Available

func (c GitHubConfig) Available() bool

type IDPlatform

type IDPlatform string
const (
	Twitter IDPlatform = "Twitter"
	GitHub  IDPlatform = "GitHub"
	OIDC    IDPlatform = "OIDC"
)

type IdentityRegister

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

func NewIdentityRegister

func NewIdentityRegister(ctx context.Context, c *Config, out io.Writer) (*IdentityRegister, []string, error)

func NewIdentityRegisterFromConfig

func NewIdentityRegisterFromConfig(ctx context.Context, c *Config, out io.Writer) (*IdentityRegister, []string, error)

func NewIdentityRegisterFromEnv

func NewIdentityRegisterFromEnv(ctx context.Context, envs []string, out io.Writer) (*IdentityRegister, []string, error)

func (IdentityRegister) AllUsers

func (ir IdentityRegister) AllUsers() []*User

func (*IdentityRegister) FindUserByID

func (ir *IdentityRegister) FindUserByID(userID string) (*User, error)

func (*IdentityRegister) FindUserOf

func (ir *IdentityRegister) FindUserOf(idp IDPlatform, userID string) (*User, error)

type OIDCConfig

type OIDCConfig struct {
	ProviderURL  string
	ClientID     string
	ClientSecret string
}

func (OIDCConfig) Available

func (c OIDCConfig) Available() bool

type ProxyTransport

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

func (ProxyTransport) RoundTrip

func (p ProxyTransport) RoundTrip(req *http.Request) (res *http.Response, err error)

type RedisConfig

type RedisConfig struct {
	Host string
}

type RedisSessionStorage

type RedisSessionStorage struct {
}

func (RedisSessionStorage) AddLoginInfo

func (s RedisSessionStorage) AddLoginInfo(ctx context.Context, oldSessionID string, info map[string]string) (newSessionID string, err error)

func (RedisSessionStorage) FindBySessionToken

func (s RedisSessionStorage) FindBySessionToken(ctx context.Context, token string) (*Session, error)

func (RedisSessionStorage) GetUserSessions

func (s RedisSessionStorage) GetUserSessions(ctx context.Context, userID string) ([]SingleSessionData, error)

func (RedisSessionStorage) Logout

func (s RedisSessionStorage) Logout(ctx context.Context, sessionID string) error

func (RedisSessionStorage) RenewSession

func (s RedisSessionStorage) RenewSession(ctx context.Context, oldSessionID string) (sessionID string, err error)

func (RedisSessionStorage) StartLogin

func (s RedisSessionStorage) StartLogin(ctx context.Context, info map[string]string) (sessionID string, err error)

func (RedisSessionStorage) StartSession

func (s RedisSessionStorage) StartSession(ctx context.Context, oldSessionID string, user *User, r *http.Request, newLoginInfo map[string]string) (sessionID string, info map[string]string, err error)

func (RedisSessionStorage) UpdateSessionData

func (s RedisSessionStorage) UpdateSessionData(ctx context.Context, sessionID string, directives []*Directive) (err error)

type Route

type Route struct {
	Path   string
	Host   *url.URL
	Scopes []string
}

type ServerlessSessionStorage

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

func NewMemorySessionStorage

func NewMemorySessionStorage(ctx context.Context, config *Config, prefix string) (*ServerlessSessionStorage, error)

func NewServerlessSessionStorage

func NewServerlessSessionStorage(ctx context.Context, config *Config, prefix string) (*ServerlessSessionStorage, error)

func (ServerlessSessionStorage) AddLoginInfo

func (s ServerlessSessionStorage) AddLoginInfo(ctx context.Context, oldSessionID string, info map[string]string) (newSessionID string, err error)

func (*ServerlessSessionStorage) Close

func (s *ServerlessSessionStorage) Close()

func (*ServerlessSessionStorage) FindBySessionToken

func (s *ServerlessSessionStorage) FindBySessionToken(ctx context.Context, token string) (*Session, error)

func (*ServerlessSessionStorage) GetUserSessions

func (s *ServerlessSessionStorage) GetUserSessions(ctx context.Context, userID string) ([]SingleSessionData, error)

func (ServerlessSessionStorage) Logout

func (s ServerlessSessionStorage) Logout(ctx context.Context, sessionID string) error

func (ServerlessSessionStorage) RenewSession

func (s ServerlessSessionStorage) RenewSession(ctx context.Context, oldSessionID string) (newSessionID string, err error)

func (ServerlessSessionStorage) StartLogin

func (s ServerlessSessionStorage) StartLogin(ctx context.Context, info map[string]string) (sessionID string, err error)

func (*ServerlessSessionStorage) StartSession

func (s *ServerlessSessionStorage) StartSession(ctx context.Context, oldSessionID string, user *User, r *http.Request, newLoginInfo map[string]string) (sessionID string, info map[string]string, err error)

func (ServerlessSessionStorage) UpdateSessionData

func (s ServerlessSessionStorage) UpdateSessionData(ctx context.Context, sessionID string, directives []*Directive) (err error)

type Session

type Session struct {
	LoginAt      UnixTime          `json:"login_at"`
	ExpireAt     UnixTime          `json:"expire_at"`
	LastAccessAt UnixTime          `json:"last_access_at"`
	UserID       string            `json:"id"`
	DisplayName  string            `json:"name"`
	Email        string            `json:"email"`
	Organization string            `json:"org"`
	Scopes       []string          `json:"scopes"`
	Status       SessionStatus     `json:"-"`
	Data         map[string]string `json:"data"`
	// contains filtered or unexported fields
}

func GetSession

func GetSession(r *http.Request) (sid string, ses *Session)

func (*Session) AddSessionData

func (s *Session) AddSessionData(key, value string)

func (*Session) RemoveSessionData

func (s *Session) RemoveSessionData(key string)

type SessionStatus

type SessionStatus int
const (
	BeforeLogin SessionStatus = iota
	ActiveSession
	IdleTimeoutSession
	AbsoluteTimeoutSession // This is not used
)

type SessionStorage

type SessionStorage interface {
	// StartLogin is called before login session
	// info keeps information like redirect URL
	StartLogin(ctx context.Context, info map[string]string) (sessionID string, err error)
	// AddLoginInfo adds extra login information for IDP.
	AddLoginInfo(ctx context.Context, oldSessionID string, info map[string]string) (newSessionID string, err error)
	// startSessionAndRedirect is called after authorization and it renews login session ID and return info that is stored in StartLogin
	StartSession(ctx context.Context, oldSessionID string, user *User, r *http.Request, newLoginInfo map[string]string) (newSessionID string, info map[string]string, err error)
	Logout(ctx context.Context, sessionID string) error
	GetUserSessions(ctx context.Context, userID string) ([]SingleSessionData, error)
	FindBySessionToken(ctx context.Context, sessionID string) (*Session, error)
	UpdateSessionData(ctx context.Context, sessionID string, directives []*Directive) (err error)
	RenewSession(ctx context.Context, oldSessionID string) (sessionID string, err error)
}

func NewSessionStorage

func NewSessionStorage(ctx context.Context, c *Config, out io.Writer) (SessionStorage, error)

type SingleSessionData

type SingleSessionData struct {
	ID             string    `docstore:"id" json:"-"`
	UserID         string    `docstore:"user_id" json:"-"`
	LoginAt        time.Time `docstore:"login_at" json:"login_at"`
	LastAccessAt   time.Time `docstore:"last_access_at" json:"last_access_at"`
	CurrentSession bool      `docstore:"-" json:"current"`

	LoginInfo map[string]string `docstore:"loginInfo" json:"login_info"`
}

func (SingleSessionData) Browser

func (s SingleSessionData) Browser() string

func (SingleSessionData) IdP

func (s SingleSessionData) IdP() string

func (SingleSessionData) LastAccessAtForHuman

func (s SingleSessionData) LastAccessAtForHuman() string

func (SingleSessionData) LastAccessAtFormat

func (s SingleSessionData) LastAccessAtFormat() string

func (SingleSessionData) Location

func (s SingleSessionData) Location() string

func (SingleSessionData) LoginAtForHuman

func (s SingleSessionData) LoginAtForHuman() string

func (SingleSessionData) LoginAtFormat

func (s SingleSessionData) LoginAtFormat() string

func (SingleSessionData) OS

func (s SingleSessionData) OS() string

type TwitterConfig

type TwitterConfig struct {
	ConsumerKey    string
	ConsumerSecret string
}

func (TwitterConfig) Available

func (c TwitterConfig) Available() bool

type UnixTime

type UnixTime time.Time

func (UnixTime) MarshalJSON

func (u UnixTime) MarshalJSON() ([]byte, error)

type User

type User struct {
	DisplayName           string             `json:"display_name"`
	Organization          string             `json:"organization"`
	UserID                string             `json:"user_id"`
	Email                 string             `json:"email"`
	Scopes                []string           `json:"scopes"`
	FederatedUserAccounts []FederatedAccount `json:"federated_accounts"`
}

func (User) ScopeString

func (u User) ScopeString() string

func (User) WriteAsJson

func (u User) WriteAsJson(w io.Writer) error

type UserSession

type UserSession struct {
	ID       string            `docstore:"id"`
	Sessions []string          `docstore:"singleSessions"`
	Data     map[string]string `docstore:"data"`

	// User Informations
	DisplayName  string   `docstore:"name"`
	Email        string   `docstore:"email"`
	Organization string   `docstore:"org"`
	Scopes       []string `docstore:"scopes"`
}

Directories

Path Synopsis
cmd
wru

Jump to

Keyboard shortcuts

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