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
- type Client
- type Clients
- type Grants
- type Key
- type MemoryGrants
- func (g *MemoryGrants) Claim(_ context.Context, kind, key string, expires, now time.Time) (bool, error)
- func (g *MemoryGrants) Delete(_ context.Context, kind, key string) error
- func (g *MemoryGrants) Get(_ context.Context, kind, key string, now time.Time) ([]byte, bool, error)
- func (g *MemoryGrants) Prune(_ context.Context, now time.Time) error
- func (g *MemoryGrants) Put(_ context.Context, kind, key string, data []byte, expires time.Time) error
- func (g *MemoryGrants) Take(_ context.Context, kind, key string, now time.Time) ([]byte, bool, error)
- type PatientChoice
- type Server
- func (s *Server) AuthorizeURL() string
- func (s *Server) ClientList() []*Client
- func (s *Server) DaVinciVersion(clientID string) string
- func (s *Server) Handler() http.Handler
- func (s *Server) Revoked(jti string) bool
- func (s *Server) SetClients(c Clients)
- func (s *Server) SetUsers(u Users)
- func (s *Server) TokenURL() string
- func (s *Server) UpstreamRedirect() string
- func (s *Server) UserList() []*User
- func (s *Server) Withdrawn(clientID, subject string) bool
- type Upstream
- type UpstreamFile
- type User
- type Users
Constants ¶
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 ¶
Clients is the registry, keyed by id.
func LoadClients ¶
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.
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 ¶
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.
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) Delete ¶ added in v0.1.7
func (g *MemoryGrants) Delete(_ context.Context, kind, key string) 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 ¶
AuthorizeURL is where apps send people to sign in, empty when nobody can.
func (*Server) ClientList ¶ added in v0.1.7
ClientList is every registered app, by id.
func (*Server) DaVinciVersion ¶ added in v0.1.21
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) Revoked ¶
Revoked reports whether an access token was revoked; the FHIR endpoint asks on every request.
func (*Server) SetClients ¶ added in v0.1.7
SetClients replaces the registered apps. Grants already issued to an app no longer listed stop working.
func (*Server) SetUsers ¶ added in v0.1.7
SetUsers replaces the people who may sign in. Refresh tokens of someone no longer listed stop working.
func (*Server) TokenURL ¶
TokenURL is where clients ask for tokens; it is also the audience of their assertions.
func (*Server) UpstreamRedirect ¶ added in v0.1.7
UpstreamRedirect is the redirect URI to register at the provider.
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
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.