defprotocol GitHub.Auth do
@moduledoc """
Protocol for extracting API authentication tokens from application structs
Credentials can be passed to operations using the `auth` option as strings (for tokens) or
2-tuples (for client ID / secret or username / password). Sometimes, it's more convenient to pass
a struct — like as a user struct — and extract the auth token from that.
By implementing this protocol, the client can extract an auth token from the given struct without
additional work by the caller.
## Example
defimpl GitHub.Auth, for: MyApp.User do
def to_auth(%MyApp.User{github_token: token}), do: token
end
## Provided Implementations
This library provides several implementations for the protocol based on library structs.
### GitHub.App
For app structs with `id` and `pem` fields containing the GitHub App ID and private key
(respectively), the default implementation will generate a JWT compatible with certain API
endpoints. Generally, the PEM field can only be filled in manually. To assist with this, the
helper function `GitHub.app/1` will construct a valid app struct using configured values.
Creating a JWT requires the optional dependency `JOSE`.
JWTs are made to last for several minutes, so it is prudent to cache values between requests.
See `GitHub.Auth.Cache` for a built-in caching mechanism.
"""
@typedoc "Auth token accepted by the client"
@type auth ::
nil
| (token :: String.t())
| {username_or_client_id :: String.t(), password_or_client_secret :: String.t()}
@doc """
Extract an auth token from the given struct
The returned data should be in the form of a string (for a Bearer token) or a 2-tuple (for a Basic
Auth user/password pair).
"""
@spec to_auth(t) :: auth
def to_auth(value)
end
if Code.ensure_loaded?(JOSE) do
defimpl GitHub.Auth, for: GitHub.App do
@token_clock_drift_allowance_sec 60
@token_duration_sec 10 * 60
def to_auth(%{id: app_id, pem: private_key}) do
case GitHub.Auth.Cache.get({:app, app_id}) do
{:ok, jwt} ->
jwt
_else ->
time = DateTime.utc_now() |> DateTime.to_unix(:second)
expiration = time - @token_clock_drift_allowance_sec + @token_duration_sec
claims = %{
"iat" => time - @token_clock_drift_allowance_sec,
"exp" => expiration,
"iss" => app_id
}
jwk = JOSE.JWK.from_pem(private_key)
jws = JOSE.JWS.from_map(%{"alg" => "RS256", "typ" => "JWT"})
{_, jwt} =
JOSE.JWT.sign(jwk, jws, claims)
|> JOSE.JWS.compact()
GitHub.Auth.Cache.put({:app, app_id}, expiration, jwt)
jwt
end
end
end
else
defimpl GitHub.Auth, for: GitHub.App do
def to_auth(app) do
raise GitHub.Error.new(
source: app,
message: """
JOSE Not Installed
Optional dependency JOSE was not installed at the
time `GitHub.Auth` was compiled. Please ensure it
is installed and run:
`mix deps.compile --force oapi_github`
"""
)
end
end
end