OAuth2 and Mastodon
Submitted by Bill St. Clair on Mon, 01 Jul 2019 22:15:18 GMT
This page explains my thinking of doing OAuth2 authorization for an Elm web frontend talking to a Mastodon API backend.
The OAuth2 Authorization Code Grant Flow Dance
OAuth2 has a number of authentication methods. For end users, using web apps, the Authorization Code Grant Flow is common. It lets the user authenticate with the service she's using, such that the client code never sees her userid or password, then the client code fetches a token it can use to authenticate API requests. If the user notices the client misbehaving, she can go to the service's web site, and remove permission for that client to access her account.
There are three computers involved:
- The user's computer, often via a web browser.
- The client application's web server, accessed via the
redirectUri
. - The API service's web server, accessed via the
authorizationUri
, thetokenUri
, and theapiUri
.
Examples:
redirectUri: | https://xossbow.com/oath |
---|---|
authorizationUri: | https://mastodon.social/oauth/authorize |
tokenUri: | https://mastodon.social/oauth/token |
apiUri: | https://mastodon.social/api/v1 |
Mastodon requires /oauth/authorize
and /oauth/token
as the OAuth 2 endpoints and /api/v1
as the base of the REST API URLs.
There is some information that the API service uses to identify the client application:
- The application name
- The application Url (optional)
- The
clientId
- The
clientSecret
The clientId
can be present in the webapp, on the users's computer, since without the clientSecret
, it cannot be used for anything. The clientSecret
is kept secret, on the redirect server.
Steps in the dance:
- The user clicks a "Login" button in her web browser.
- The webapp redirects to the
authorizationUri
, passing theclientId
, theredirectUri
,scope
descriptors, saying what the client app will be allowed to do, and astate
string. - The
authorizationUri
puts up a form, requesting userid and password. - If the user logs in successfully, the
authorizationUri
server forwards the user's browser to theredirectUri
, passing theclientId
,state
, and an authenticationcode
that it generates and remembers. - The
redirectUri
server posts theclientId
,clientSecret
, and authenticationcode
to thetokenUri
, and receives back atoken
. - The
redirectUri
server then uses the state to put up a web page for the user to interact with the server via the API. - When the user does something that requires an API call, the
token
is passed along, for authentication and identification of the user.
The Status Quo
First, I'll explain from where I started, back in December of 2017, before adding Mastodon to the mix.
I published the billstclair/elm-oauth-middleware in the public Elm repository. It works with Google, Facebook, GitHub, Gab Legacy, and likely any other proper implementation of the OAuth2 Authorization Code Grant Flow (but I only tested those four). It is running at https://xossbow.com/oath
. It contains both server and client code.
The elm-oauth-middleware server expects application definitions to be mostly static, with the tokenUri
, clientId
, and clientSecret
stored in a JSON file on the server, which is queried periodically, and reloaded if it changes. This allows hot changes to the applications, with a text editor on the server.
I store the clientId
, and redirectUri
in the Elm client code, compiled to JavaScript in the browser, again loaded from a JSON file that ships with the client application. It redirects to the authorizationUri
, passing the clientId
, redirectUri
, scope
, and some state
. The authorization server (Google, Facebook, GitHub, Gab, etc.) prompts the user for ID and password, and redirects to the redirectUri
with an authorization code
and some Base64 encoded JSON state. That server posts the code
to the tokenUri
to get a token, which it returns to the client code, by using the state
to go to a URL on a redirectBackHost
that are validated from its configuration file. Validation of that redirectBackHost
is my invention.
This is a non-standard use of the Authorization Code Grant Flow. Usually, the token stays on the server, and it uses it to make API calls and then populate HTML for the client browser. Since my clients are all in Elm, and work with no HTML generation by a server, other than an initial static HTML file that loads the Elm JavaScript, that client needs to have the token, and make calls itself to the apiUri
. The client typically stores the token in JavaScript localStorage
, so that it doesn't have to request it again every time the user goes to its web site.
Enter Mastodon
Normal Mastodon servers have a Your Applications page, linked from </> Development
in the left column of the preferences page. This allows you to create a standard, static, OAuth2 application, giving an Application name
, Application website
, Redirect URI
list, and allowed Scopes
, and receiving a clientId
, clientSecret
, and token
.
This is fine if your application is targeted to a small set of Mastodon servers, but the nature of Mastodon is hundreds of federated servers, so the API has a POST /api/v1/apps
call to create a new client_id
and client_secret
.
My idea for using this is to have the Elm app send its own URL as the Redirect URI, use POST /api/v1/apps
to get a client ID and secret, then redirect to https://<mastodon-host>/oauth/authorize
, so the user can log in, get back the authorization code when restarted as the redirectUri
, then POST
to https://<mastodon-host>/oauth/token
to turn that code into a token. The only thing I don't now yet is whether that final POST will pass CORS muster. The API calls have to, but that one doesn't. If it doesn't, then that part of the dance needs to be moved to a server, with the clientId
and clientSecret
passed in the state, so that the server doesn't need any state itself.
This is likely an unusual use of the POST /api/v1/apps
call. I think it's expected that the redirectUri
host will save the association of the <mastodon-host>
and a clientId/clientSecret
pair, so that it doesn't need to request a new one unless a new user specifies a never-before-seen server. I plan to cache the clientId/clientSecret
pair (and the most recently issued token
) in localStorage
on the user's machine, but store no state on the server, even if I need one to get around CORS.
Comments (1)
It works!
Submitted by Bill St. Clair on Tue, 23 Jul 2019 14:22:50 GMT
I have implemented this idea in billstclair/elm-mastodon. It works against Mastodon and Pleroma servers. It is live at https://mammudeck.com/api
Edit comment