smartauth

package
v0.1.23 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package smartauth is a SMART App Launch authorization server: it issues the access tokens Perfuse's FHIR endpoint accepts.

Written against the standard library, like internal/oidc, which verifies what this signs. The tokens are JWTs signed RS256, the one algorithm every SMART client and the specification's ID token rules can rely on.

Index

Constants

View Source
const (
	// KindBackend is SMART Backend Services: no person, a signed JWT assertion, client_credentials.
	KindBackend = "backend"
	// KindPublic is an app that cannot keep a secret (in a browser or on a phone): PKCE and nothing else.
	KindPublic = "public"
	// KindSymmetric is a confidential app authenticating with a client secret.
	KindSymmetric = "confidential-symmetric"
	// KindAsymmetric is a confidential app authenticating with a signed JWT assertion.
	KindAsymmetric = "confidential-asymmetric"
)

Client kinds, as SMART App Launch names them.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	ID   string `yaml:"id"`
	Name string `yaml:"name"`
	Kind string `yaml:"kind"`
	// Scopes are the most this client may be granted, matched exactly; one ending in * covers every scope beginning with
	// what comes before it ("system/*" covers every system scope).
	Scopes []string `yaml:"scopes"`
	// JWKSURI or JWKS hold the client's public keys, for backend and asymmetric clients.
	JWKSURI string         `yaml:"jwks_uri"`
	JWKS    map[string]any `yaml:"jwks"`
	// SecretHash is HashPassword's output for a symmetric client's secret; the secret itself is never stored.
	SecretHash   string   `yaml:"secret_hash"`
	RedirectURIs []string `yaml:"redirect_uris"`
	LaunchURL    string   `yaml:"launch_url"`
	// DaVinciVersion is the release of the Da Vinci guides (CRD, DTR, PAS) this client speaks, "2.1" or "2.2", when the
	// requests it sends do not say; empty leaves it to the server's -davinci-version.
	DaVinciVersion string `yaml:"davinci_version"`
	// contains filtered or unexported fields
}

Client is one registered app.

type Clients

type Clients map[string]*Client

Clients is the registry, keyed by id.

func LoadClients

func LoadClients(path string) (Clients, error)

LoadClients reads the clients file, refusing what would fail later at a token request: a backend client with no keys, a public client with a secret, a redirect URI that is not absolute.

func SaveClients added in v0.1.7

func SaveClients(path string, list []*Client) (Clients, error)

SaveClients writes the clients file and returns the registry as loaded from it.

type Grants added in v0.1.7

type Grants interface {
	Put(ctx context.Context, kind, key string, data []byte, expires time.Time) error
	Get(ctx context.Context, kind, key string, now time.Time) ([]byte, bool, error)
	// Take reads and removes, atomically: a code or launch id works once.
	Take(ctx context.Context, kind, key string, now time.Time) ([]byte, bool, error)
	// Claim stores a key only if no live one is there, reporting whether it did: an assertion id is used once even when two
	// requests race with it.
	Claim(ctx context.Context, kind, key string, expires, now time.Time) (bool, error)
	Delete(ctx context.Context, kind, key string) error
	Prune(ctx context.Context, now time.Time) error
}

Grants keeps what the authorization server has handed out and must remember: authorization codes, refresh tokens, revoked access tokens, used client assertion ids and EHR launch ids. In memory by default; serve gives it the database (store.SMARTGrants), so a restart neither signs everyone out nor forgets a revocation.

Keys reach it already hashed (hashKey): the database holds no code or token that could be presented.

type Key

type Key struct {

	// ID is the key's JWK thumbprint (RFC 7638), so a token names the key it was signed with and a rotated key is told apart.
	ID string
	// contains filtered or unexported fields
}

Key is the server's signing key.

func LoadOrCreateKey

func LoadOrCreateKey(path string) (*Key, error)

LoadOrCreateKey reads a PEM private key, or creates a 2048-bit RSA key there (mode 0600) when the file does not exist. A key file anyone else may read is refused: whoever holds it can sign tokens for every patient.

func (*Key) JWKS

func (k *Key) JWKS() []byte

JWKS is the public half, as /auth/jwks serves it.

func (*Key) Sign

func (k *Key) Sign(claims map[string]any) (string, error)

Sign returns a compact RS256 JWT carrying the claims.

type MemoryGrants added in v0.1.7

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

MemoryGrants is Grants in memory, for tests and for a server with no database.

func (*MemoryGrants) Claim added in v0.1.7

func (g *MemoryGrants) Claim(_ context.Context, kind, key string, expires, now time.Time) (bool, error)

func (*MemoryGrants) Delete added in v0.1.7

func (g *MemoryGrants) Delete(_ context.Context, kind, key string) error

func (*MemoryGrants) Get added in v0.1.7

func (g *MemoryGrants) Get(_ context.Context, kind, key string, now time.Time) ([]byte, bool, error)

func (*MemoryGrants) Prune added in v0.1.7

func (g *MemoryGrants) Prune(_ context.Context, now time.Time) error

func (*MemoryGrants) Put added in v0.1.7

func (g *MemoryGrants) Put(_ context.Context, kind, key string, data []byte, expires time.Time) error

func (*MemoryGrants) Take added in v0.1.7

func (g *MemoryGrants) Take(_ context.Context, kind, key string, now time.Time) ([]byte, bool, error)

type PatientChoice

type PatientChoice struct {
	ID, Name, BirthDate string
}

PatientChoice is one patient a clinician can pick.

type Server

type Server struct {
	// Issuer is this server's identifier, the iss of every token: the public URL of its base, such as https://host/auth.
	Issuer string
	// Audience is the FHIR base the tokens are for.
	Audience string
	Key      *Key
	Clients  Clients
	Now      func() time.Time

	// Users sign in to authorize apps; without any, only backend clients get tokens.
	Users Users
	// Patients lists patients a clinician may pick, matching a search text; PatientExists checks a pick.
	Patients      func(ctx context.Context, search string) ([]PatientChoice, error)
	PatientExists func(ctx context.Context, id string) bool

	// Upstream, when set, offers sign-in at the organisation's identity provider beside the password.
	Upstream *Upstream

	// Grants keeps codes, refresh tokens, revocations, used assertion ids and launch ids; in memory when nil.
	Grants Grants
	// contains filtered or unexported fields
}

Server is the authorization server.

func (*Server) AuthorizeURL

func (s *Server) AuthorizeURL() string

AuthorizeURL is where apps send people to sign in, empty when nobody can.

func (*Server) ClientList added in v0.1.7

func (s *Server) ClientList() []*Client

ClientList is every registered app, by id.

func (*Server) DaVinciVersion added in v0.1.21

func (s *Server) DaVinciVersion(clientID string) string

DaVinciVersion is the Da Vinci release a registered client speaks, from its davinci_version, or "" when the client is unknown or its registration says nothing.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler serves /token and /jwks under the issuer's path.

func (*Server) Revoked

func (s *Server) Revoked(jti string) bool

Revoked reports whether an access token was revoked; the FHIR endpoint asks on every request.

func (*Server) SetClients added in v0.1.7

func (s *Server) SetClients(c Clients)

SetClients replaces the registered apps. Grants already issued to an app no longer listed stop working.

func (*Server) SetUsers added in v0.1.7

func (s *Server) SetUsers(u Users)

SetUsers replaces the people who may sign in. Refresh tokens of someone no longer listed stop working.

func (*Server) TokenURL

func (s *Server) TokenURL() string

TokenURL is where clients ask for tokens; it is also the audience of their assertions.

func (*Server) UpstreamRedirect added in v0.1.7

func (s *Server) UpstreamRedirect() string

UpstreamRedirect is the redirect URI to register at the provider.

func (*Server) UserList added in v0.1.7

func (s *Server) UserList() []*User

UserList is every person who may sign in, by username.

func (*Server) Withdrawn added in v0.1.7

func (s *Server) Withdrawn(clientID, subject string) bool

Withdrawn reports whether the app a token was issued to, or the person who authorized it, has been removed since. A backend token's subject is its client; a user token's is the username.

type Upstream added in v0.1.7

type Upstream struct {
	// Label is the button's text: "Sign in with <Label>".
	Label        string
	ClientID     string
	ClientSecret string
	Scopes       []string
	Provider     *oidc.Provider
	Keys         *oidc.KeySet
	// HTTP is the client for the token exchange; nil for the default.
	HTTP *http.Client
}

Upstream is an OpenID Connect provider people may sign in with instead of a password: the organisation's own identity provider (Entra ID, Okta, Keycloak). The person must still be listed in the users file, linked by oidc_subject, because the users file is what says which FHIR resource they are; a provider account with no link is refused, never created.

func LoadUpstream added in v0.1.7

func LoadUpstream(ctx context.Context, path string) (*Upstream, error)

LoadUpstream reads the -smart-oidc file and discovers the provider. The secret comes from a file or an environment variable, never the file itself, which is often templated or kept in a repository.

type UpstreamFile added in v0.1.7

type UpstreamFile struct {
	Issuer           string   `yaml:"issuer"`
	ClientID         string   `yaml:"client_id"`
	ClientSecretFile string   `yaml:"client_secret_file"`
	ClientSecretEnv  string   `yaml:"client_secret_env"`
	Label            string   `yaml:"label"`
	Scopes           []string `yaml:"scopes"`
}

UpstreamFile is the -smart-oidc file.

type User

type User struct {
	Username string `yaml:"username"`
	Name     string `yaml:"name"`
	// PasswordHash is perfuse smart hash's output; the password itself is never stored.
	PasswordHash string `yaml:"password_hash"`
	// FHIRUser is the FHIR resource the person is, Patient/123 or Practitioner/456. A Patient user only ever gets their own
	// record, whatever scopes an app asks for.
	FHIRUser string `yaml:"fhir_user"`
	// OIDCSubject links the person to their account at the upstream identity provider (-smart-oidc): the ID token's sub,
	// which is stable, never an email that can be reassigned. Either this or a password_hash is needed.
	OIDCSubject string `yaml:"oidc_subject,omitempty"`
}

User is a person who signs in to authorize an app: a member reading their own record, or a clinician.

type Users

type Users map[string]*User

Users is keyed by username.

func LoadUsers

func LoadUsers(path string) (Users, error)

LoadUsers reads the users file.

func SaveUsers added in v0.1.7

func SaveUsers(path string, list []*User) (Users, error)

SaveUsers writes the users file and returns the people as loaded from it.

Jump to

Keyboard shortcuts

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