server

package module
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Jan 9, 2024 License: MIT Imports: 36 Imported by: 6

README

go-authserver

A library to easily create an authenticated web server in Go

Supports arbitrary username&password type authentication using your own callback to veryify the password, and also Okta auth via both a CLI and a web interface.

The server is gin-based, and you add routes to the server using Router() or AuthRouter(), then Start() it (it will gracefully stop on SIGINT and SIGTERM):

import gas "github.com/wtsi-hgi/go-authserver"

logger := syslog.new(syslog.LOG_INFO, "tag")

server := gas.New(logger)

server.Router().GET(gas.EndPointREST+"/myendpoint", myGinHandlerFunc)

server.EnableAuth("cert.pem", "key.pem", func(username, password string) (bool, string) {
    return true, "" // allows all login attempts; do proper password checking instead!
})

server.AuthRouter().GET("/mysecuredendpoint", myGinHandlerFuncForSecureStuff)

err := server.Start("localhost:8080", "cert.pem", "key.pem")

With the server running, a client can login with a username and password:

import gas "github.com/wtsi-hgi/go-authserver"

jwt, err := gas.Login("localhost:8080", "cert.pem", "username", "password")

restyRequest := gas.NewAuthenticatedClientRequest("localhost:8080", "cert.pem", jwt)

response, err := restyRequest.Get(gas.EndPointAuth+"/mysecuredendpoint")

Okta

For okta auth, you will need an Okta app configured like:

  • Sign-in method: OIDC
  • App type: Web application
  • Name: [your app name]
  • Grant type: Authorization code
  • Sign-in redirect URIs: https://[your domain:port]/callback, https://[your domain:port]/callback-cli
  • Sign-out redirect URIs: https://[your domain:port]/
  • Assignments: allow everyone access

Then for the server, after calling EnableAuth(), also say:

server.AddOIDCRoutes(oktaURL, oktaOAuthIssuer, oktaOAuthClientID, oktaOAuthClientSecret)

Then a command-line client can log in using Okta after getting a code by visiting https://localhost:8080/login-cli :

jwt, err := gas.LoginWithOKTA("localhost:8080", "cert.pem", code)

A web-based client can log in by visiting https://localhost:8080/login . After logging in they will be redirected to your default route.

Documentation

Index

Constants

View Source
const (
	ErrBadJWTClaim     = Error("JWT had bad claims")
	ErrEmailNotPresent = Error("field `email` not present")
)
View Source
const (
	ErrCouldNotVerifyToken = Error("token could not be verified")
	ErrOIDCUnexpectedState = Error("the state was not as expected")
	ErrOIDCUnavailableCode = Error("the code was not returned or is not accessible")
	ErrOIDCMissingToken    = Error("id token missing from OAuth2 token")
	ErrJSONValueNotString  = Error("non-string value in JSON field")
	ErrOIDCBadMeta         = Error("issuer meta information not found")
)
View Source
const (
	// EndPointREST is the base location for all REST endpoints.
	EndPointREST = "/rest/v1"

	// EndPointJWT is the endpoint for creating or refreshing a JWT.
	EndPointJWT = EndPointREST + "/jwt"

	// EndPointAuth is the name of the router group that endpoints requiring JWT
	// authorisation should belong to.
	EndPointAuth = EndPointREST + "/auth"

	// EndpointOIDCLogin will be handled by redirecting the user to Okta.
	EndpointOIDCLogin = "/login"

	// EndpointOIDCCLILogin will be handled by redirecting the user to Okta,
	// to get an auth code back to copy paste.
	EndpointOIDCCLILogin = "/login-cli"

	// EndpointAuthCallback is the endpoint where the OIDC provider will
	// send the user back to after login.
	EndpointAuthCallback    = "/callback"
	EndpointAuthCLICallback = "/callback-cli"

	// EndpointCLIAuthCode is the endpoint the user can get an auth code from
	// to copy paste into the terminal for a CLI session.
	EndpointCLIAuthCode = "/auth-code"

	ErrNeedsAuth = Error("authentication must be enabled")
)
View Source
const ClientProtocol = "https://"
View Source
const DevEnvKey = "GAS_DEV"
View Source
const DevEnvVal = "1"
View Source
const ErrBadQuery = Error("bad query")
View Source
const ErrNoAuth = Error("authentication failed")

Variables

This section is empty.

Functions

func CreateTestCert added in v1.0.2

func CreateTestCert(t *testing.T) (string, string, error)

CreateTestCert creates a self-signed cert and key in a temp dir and returns their paths.

func GenerateAndStoreTokenForSelfClient added in v1.1.0

func GenerateAndStoreTokenForSelfClient(tokenFile string) ([]byte, error)

GenerateAndStoreTokenForSelfClient calls GenerateToken() and returns the token, but also stores it in the given file, readable only by the current user. You could call this when starting a Server, and then in your AuthCallback verify a client trying to login by comparing their "password" against the token, using TokenMatches(). (Using EnableAuthWithServerToken() does all this for you.)

A command line client started by the same user that started the Server would then be able to login by getting the token using GetStoredToken(), and using that as its "password".

If the given tokenFile already exists, and contains a single 43 byte string, then that is re-used as the token instead.

func GenerateToken added in v1.1.0

func GenerateToken() ([]byte, error)

GenerateToken creates a cryptographically secure pseudorandom URL-safe base64 encoded string 43 bytes long. Returns it as a byte slice.

func GetStoredToken added in v1.1.0

func GetStoredToken(tokenFile string) ([]byte, error)

GetStoredToken reads the token from the given file but only returns it if it's got some length.

We also check if the file has private permissions, otherwise we won't use it. This is as an attempt to reduce the likelihood of the token being leaked with its long expiry time (used so the user doesn't have to continuously log in, as we're not working with specific refresh tokens to get new access tokens).

func GetUser added in v1.0.2

func GetUser(t *testing.T) (string, string)

GetUser returns the current user's username and uid.

func IncludeAbortErrorsInBody added in v1.2.0

func IncludeAbortErrorsInBody(c *gin.Context)

IncludeAbortErrorsInBody is a gin.HandlerFunc that can be Use()d with gin routers from Router() and AuthRouter() that ensures that the errors we accumulate in AbortWithError() calls get written to the returned body.

func Login

func Login(r *resty.Request, username, password string) (string, error)

Login is a client call to a Server listening at the domain:port url given to the request that checks the given password is valid for the given username, and returns a JWT if so.

Make the request using NewClientRequest() and a non-blank path to a certificate to force us to trust that certificate, eg. if the server was started with a self-signed certificate.

func LoginWithOKTA

func LoginWithOKTA(r *resty.Request, username, token string) (string, error)

LoginWithOKTA sends a request to the server containing the token as a cookie, so it will be able to return the JWT for the user. The request should have been made with an addr that is just the domain:port that was used to Start() the server.

Make the request using NewClientRequest() and a non-blank path to a certificate to force us to trust that certificate, eg. if the server was started with a self-signed certificate.

func NewAuthenticatedClientRequest

func NewAuthenticatedClientRequest(url, cert, jwt string) *resty.Request

NewAuthenticatedClientRequest is like NewClientRequest, but sets the given JWT in the authorization header.

func NewClientRequest

func NewClientRequest(url, cert string) *resty.Request

NewClientRequest creates a resty Request that will trust the certificate at the given path. cert can be blank to only trust the normal installed cert chain.

func QueryREST added in v1.0.2

func QueryREST(router *gin.Engine, endpoint, extra string) (*httptest.ResponseRecorder, error)

QueryREST does a test GET of the given REST endpoint (start it with /), with extra appended (start it with ?). router can be Server.Router().

func RefreshJWT

func RefreshJWT(url, cert, token string) (string, error)

RefreshJWT is like Login(), but refreshes a JWT previously returned by Login() if it's still valid.

func StartTestServer added in v1.0.2

func StartTestServer(s StartStop, certPath, keyPath string) (string, func() error, error)

StartTestServer starts the given server using the given cert and key paths and returns the address and a func you should defer to stop the server.

func TokenDir added in v1.3.0

func TokenDir() (string, error)

TokenDir is the directory where the server will store a token file when using GenerateAndStoreTokenForSelfClient(), and ClientCLI will store JWTs. It is the value of XDG_STATE_HOME, falling back to the user's HOME directory.

func TokenMatches added in v1.1.0

func TokenMatches(input, expected []byte) bool

TokenMatches compares two tokens and tells you if they match. Does so in a cryptographically secure way (avoiding timing attacks).

func UserNameToUID added in v1.0.1

func UserNameToUID(name string) (string, error)

UserNameToUID converts user name to UID.

Types

type AuthCallback

type AuthCallback func(username, password string) (bool, string)

AuthCallback is a function that returns true if the given password is valid for the given username. It also returns the user's UID.

type ClientCLI added in v1.3.0

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

ClientCLI can be used by a CLI client to log in to a go-authserver Server.

func NewClientCLI added in v1.3.0

func NewClientCLI(jwtBasename, serverTokenBasename, url, cert string, oktaMode bool) (*ClientCLI, error)

NewClientCLI returns a ClientCLI that will get and store JWTs from and to a file with the given basename in the user's XDG_STATE_HOME or HOME directory, initially retrieving the JWT from the server at url using cert.

If the user needs to login (no valid JWT found), asks user for the password or an oktaCode if oktaMode is true.

The normal password checking procedure will be bypassed if the current user is the same one that started the server, the server used EnableAuthWithServerToken(), and the given serverTokenBasename file in XDG_STATE_HOME or HOME contains the server's token.

func (*ClientCLI) AuthenticatedRequest added in v1.3.0

func (c *ClientCLI) AuthenticatedRequest() (*resty.Request, error)

NewAuthenticatedRequest logs in to our server if needed to get the jwt, and returns an authenticated request.

func (*ClientCLI) CanReadServerToken added in v1.3.0

func (c *ClientCLI) CanReadServerToken() bool

CanReadServerToken returns true if this user can read the server token file and the token is the correct length. Does NOT check with the server if it's actually correct. Use this as a shortcut prior to trying to login for a CLI command that's only intended for use by the user who started a server.

func (*ClientCLI) GetJWT added in v1.3.0

func (c *ClientCLI) GetJWT() (string, error)

GetJWT checks if we have stored a jwt in our file. If so, the JWT is refreshed and returned.

Otherwise, we ask the user for the password/code and login, storing and returning the new JWT.

func (*ClientCLI) Login added in v1.3.0

func (c *ClientCLI) Login(usernameAndPassword ...string) error

Login either asks the user for a password or for their okta code and logs in to our server with it. If this user started the server, gets the "password" from the server token file instead. If the optional username and password are supplied (for testing purposes), uses those instead of asking on the terminal.

type Error

type Error string

func (Error) Error

func (e Error) Error() string

type JWTPermissionsError added in v1.3.0

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

JWTPermissionsError is used to distinguish this type of error - where the already stored JWT token doesn't have private permissions.

func (JWTPermissionsError) Error added in v1.3.0

func (e JWTPermissionsError) Error() string

Error is the print out string for JWTPermissionsError, so the user can see and rectify the permissions issue.

type OktaUser

type OktaUser struct {
	Email string `json:"email"`
}

OktaUser is used to json.Unmarshal Okta user claims.

type PasswordHandler added in v1.3.0

type PasswordHandler interface {
	Prompt(string, ...interface{})
	ReadPassword() ([]byte, error)
	IsTerminal() bool
}

PasswordHandler can ask for and return a password read from a reader.

type Server

type Server struct {
	Logger *log.Logger
	// contains filtered or unexported fields
}

Server is used to start a web server that provides a REST API for authenticating, and a router you can add website pages to.

func New

func New(logWriter io.Writer) *Server

New creates a Server which can serve a REST API and website.

It logs to the given io.Writer, which could for example be syslog using the log/syslog pkg with syslog.new(syslog.LOG_INFO, "tag").

func (*Server) AddOIDCRoutes

func (s *Server) AddOIDCRoutes(addr, issuer, clientID, clientSecret string)

AddOIDCRoutes creates the OAuth environments for both the web app and the CLI and adds the login and callback endpoints, along with an endpoint to get an auth code for the CLI. Addr should be the same domain:port as later supplied to Start().

func (*Server) AddStaticPage

func (s *Server) AddStaticPage(staticFS embed.FS, rootDir, path string)

AddStaticPage adds the given document root to the Router() at the given absolute query path. Files within the document root will then be served.

The files will be embedded by default, using the given embed.FS. You can create one of these by saying in your package: //go:embed static var staticFS embed.FS

For a live view of the files in a running server, set the env var GAS_DEV to 1.

func (*Server) AllowedAccess added in v1.3.0

func (s *Server) AllowedAccess(c *gin.Context, user string) bool

AllowedAccess gets our current user if we have EnableAuth(), and returns true if that matches the given username. Always returns true if we have not EnableAuth(), or if our current user is the user who started the Server. If user is blank, it's a test if the current user started the Server.

func (*Server) AuthRouter

func (s *Server) AuthRouter() *gin.RouterGroup

AuthRouter returns a router for authenticed end points. Use it to add end points that are sub-paths of EndPointAuth (providing just the relative path to eg. GET()). This will return nil until you've called EnableAuth().

func (*Server) EnableAuth

func (s *Server) EnableAuth(certFile, keyFile string, acb AuthCallback) error

EnableAuth adds the /rest/v1/jwt POST and GET endpoints to the REST API.

The /rest/v1/jwt POST endpoint requires the username and password parameters in a form or as JSON. It passes these to the given auth callback, and if it returns true, a JWT is returned (as a JSON string) in the response that contains Username and UIDs (comma separated strings).

Alternatively, you can POST with an oktaCookieName cookie with a value of the okta auth code from the auth-code endpoint. If the code is valid, likewise returns a JWT. You'll also need to call AddOIDCRoutes() for this scheme to work.

Queries to endpoints that need authorisation should include the JWT in the authorization header as a bearer token. Those endpoints can be implemented by extracting the *User information out of the JWT using getUser().

JWTs are signed and verified using the given cert and key files.

GET on the endpoint will refresh the JWT. JWTs expire after 5 days, but can be refreshed up until day 10 from issue.

func (*Server) EnableAuthWithServerToken added in v1.3.0

func (s *Server) EnableAuthWithServerToken(certFile, keyFile, tokenBasename string, acb AuthCallback) error

EnableAuthWithServerToken is like EnableAuth(), but also stores the current username as the "server" user who can login with a server token that will be generated and stored in a file called tokenBasename in TokenDir(), instead of via auth callback or okta.

func (*Server) GetUser added in v1.0.1

func (s *Server) GetUser(c *gin.Context) *User

GetUser retrieves the *User information extracted from the JWT in the auth header. This will only be present after calling EnableAuth(), on a route in the authGroup.

func (*Server) Router

func (s *Server) Router() *gin.Engine

Router returns a router for non-authenticed end points. Use it to add end points that are sub-paths of EndPointREST (providing the full path to eg. GET()).

func (*Server) SetStopCallBack

func (s *Server) SetStopCallBack(cb StopCallback)

func (*Server) Start

func (s *Server) Start(addr, certFile, keyFile string) error

Start will start listening to the given address (eg. "localhost:8080"), and serve the REST API and website over https; you must provide paths to your certficate and key file.

It blocks, but will gracefully shut down on SIGINT and SIGTERM. If you Start() in a go-routine, you can call Stop() manually.

func (*Server) Stop

func (s *Server) Stop()

Stop() gracefully stops the server after Start(), and waits for active connections to close and the port to be available again. It also calls any callback you set with SetStopCallBack().

type StartStop added in v1.0.2

type StartStop interface {
	Start(addr, certFile, keyFile string) error
	Stop()
}

StartStop is an interface that Server satisfies.

type StdPasswordHandler added in v1.3.0

type StdPasswordHandler struct{}

StdPasswordHandler is the default password handler using stdout and stdin.

func (StdPasswordHandler) IsTerminal added in v1.3.0

func (p StdPasswordHandler) IsTerminal() bool

IsTerminal returns true if stdin is a terminal (and thus ReadPassword() is possible).

func (StdPasswordHandler) Prompt added in v1.3.0

func (p StdPasswordHandler) Prompt(msg string, a ...interface{})

Prompt outputs the given string to stdout, formatting it with any given vars.

func (StdPasswordHandler) ReadPassword added in v1.3.0

func (p StdPasswordHandler) ReadPassword() ([]byte, error)

ReadPassword reads a password from stdin.

type StopCallback

type StopCallback func()

StopCallback is a function that you can give SetStopCallback() to have this function called when the Server is Stop()ped.

type StringLogger

type StringLogger struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

StringLogger is a thread-safe logger that logs to a string. Useful for testing Server logging.

func NewStringLogger

func NewStringLogger() *StringLogger

NewStringLogger returns a new StringLogger.

func (*StringLogger) Reset

func (s *StringLogger) Reset()

Reset passes through to our strings.Builder while being thread-safe.

func (*StringLogger) String

func (s *StringLogger) String() string

String passes through to our strings.Builder while being thread-safe.

func (*StringLogger) Write

func (s *StringLogger) Write(p []byte) (n int, err error)

Write passes through to our strings.Builder while being thread-safe.

type User

type User struct {
	Username string
	UID      string
}

User is what we store in our JWTs.

func (*User) GIDs

func (u *User) GIDs() ([]string, error)

GIDs returns the unix group IDs that our UID belongs to (unsorted, with no duplicates).

Jump to

Keyboard shortcuts

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