Skip to content

Add HTTP Basic auth as an alternative to OAuth - #109

Merged
adriankumpf merged 2 commits into
masterfrom
add-basic-auth
Aug 20, 2026
Merged

Add HTTP Basic auth as an alternative to OAuth#109
adriankumpf merged 2 commits into
masterfrom
add-basic-auth

Conversation

@adriankumpf

@adriankumpf adriankumpf commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Every endpoint accepts the account's email and password in an ordinary Authorization header, which sidesteps the token lifecycle entirely: nothing expires, there is no consumer to register, and neither of the per-IP rate limits on consumer_token and authorize applies. That is most of what Quirks of the API warns about, gone.

client = Discovergy.Client.new() |> Discovergy.Client.basic_auth(email, password)

Verified against meters, field_names, last_reading, readings, statistics, devices, disaggregation and activities, all 200. The 401 for a missing credential advertises the scheme itself, so this is not an accident of the routing layer:

www-authenticate: Basic realm="Discovergy", charset="UTF-8"

The API documents none of it, which is why this is an alternative to login/3 rather than a replacement. login/3 stays the documented way in, and everything the guide says about tokens still applies whenever it is used.

Nothing goes out when the client is built, so basic_auth/3 returns the client rather than {:ok, client}. A wrong password therefore surfaces on the first real request, as a 401 that says what is wrong:

401 Unauthorized: Invalid email or password supplied in the HTTP Authorization header

That body is the whole difference in error handling. An expired OAuth token is an empty-bodied 401 worth recovering from; a 401 here is a credential that will not start working on its own.

One auth field instead of two

:consumer and :token collapse into a single :credentials field holding either an OAuth session or the Basic auth pair. Only two of the four field combinations were ever legal, and the request path now dispatches on one value instead of picking its way through them. Client also stops knowing how either scheme builds a header: OAuth.authorization/4 and BasicAuth.authorization/1 do that themselves, both returning a lowercase header name to match the rest of the request.

credentials/1 reads the session back for persistence, which the struct being @opaque had previously left to reaching into documented fields:

- # persist client.consumer and client.token, then later:
- client = Discovergy.Client.new(consumer: consumer, token: token)
+ # persist Discovergy.Client.credentials(client), then later:
+ client = Discovergy.Client.new(credentials: credentials)

Reauthorizing a Basic auth client reports :not_logged_in, since there is no OAuth session to renew.

Verification

Both paths exercised live against the public demo account, plus an OAuth session serialized and restored into a fresh client.

Every endpoint accepts the account's email and password in an ordinary
Authorization header, which sidesteps the token lifecycle entirely:
nothing expires, there is no consumer to register, and neither of the
per-IP rate limits on consumer_token and authorize applies. Verified
against meters, field_names, last_reading, readings, statistics,
devices, disaggregation and activities. The 401 for a missing
credential advertises the scheme itself:

    www-authenticate: Basic realm="Discovergy", charset="UTF-8"

The API documents none of this, so basic_auth/3 is an alternative to
login/3 rather than a replacement, and login/3 stays the documented way
in. A wrong password comes back as a 401 that says so, unlike the
empty-bodied 401 of an expired token, so the two never need telling
apart. Nothing goes out when the client is built, so basic_auth/3
returns the client rather than {:ok, client}.

The :consumer and :token fields collapse into a single :credentials
field holding either an OAuth session or the Basic auth pair. Only two
of the four combinations were ever legal, and the request path now
dispatches on one value instead of picking its way through them. The
client no longer knows how either scheme builds a header:
OAuth.authorization/4 and BasicAuth.authorization/1 do that themselves,
both returning a lowercase name to match the rest of the request.

credentials/1 reads the session back for persistence, which the struct
being opaque had previously left to reaching into documented fields.
Reauthorizing a Basic auth client reports :not_logged_in, since there
is no OAuth session to renew.
It prescribed :erlang.term_to_binary/1 on the grounds that the structs
"redact themselves when inspected and encode to no other format", which
confuses two unrelated things: Inspect governs display, and
term_to_binary never consults it. It was also wrong for the common
case, since a term kept in ETS, a GenServer, :persistent_term or an
Agent is never serialized at all, and the warning about blobs from
older versions was generic struct advice rather than anything about
this library.

How a caller stores an opaque term is not the library's business. What
is: an OAuth session saves two rate limited calls, and a Basic auth one
is not worth storing at all.
@adriankumpf
adriankumpf merged commit ca48161 into master Aug 20, 2026
4 checks passed
@adriankumpf
adriankumpf deleted the add-basic-auth branch August 20, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant