Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tachyon oauth #335

Merged
merged 27 commits into from
Aug 8, 2024
Merged

Conversation

geekingfrog
Copy link
Contributor

@geekingfrog geekingfrog commented Jun 16, 2024

This implements the two oauth flows required for running tachyon.

There's also at the time of writing, a demo server running at https://tachyon.geekingfrog.com:4567/login

There's currently only one OAuth application registered, with the hardcoded scope tachyon.lobby, one redirect uri: http://127.0.0.1/oauth2callback and the client id is generic_lobby.

The lobby flow

This is using the authorization_code flow.

getting an authorization code

First, the client need to generate a PKCE verifier and challenge.
For example:

challenge: "BGLMtLONQ_f6-Z6ikTk8ofWo-cWM3UUeT93LIEG33-M"
verifier: "2ENOENOGA0USUNPROMSUD9U64P604R2LVOVDG5SEL7EIGA5SL3TC2BQN0MJVVG8S"

Then, the client can request an authorization code with a GET request to the endpoint:

https://tachyon.geekingfrog.com:4567/oauth/authorize?response_type=code&code_challenge=BGLMtLONQ_f6-Z6ikTk8ofWo-cWM3UUeT93LIEG33-M&code_challenge_method=S256&client_id=generic_lobby&redirect_uri=http%3A%2F%2F127.0.0.1%2Foauth2callback

You get redirected to a login screen.
The demo server has one user with email/password: [email protected] and password: tachyonmelon

You then should see a screen to grant access:

2024-06-16-856x269-scrot

Clicking on "Let's go!" redirects to: 127.0.0.1/oauth2callback?code=<code>

exchanging the authorization code for an access token

You can then issue a POST request to

curl -iv https://tachyon.geekingfrog.com:4567/oauth/token \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "client_id=generic_lobby" \
--data-urlencode "code=60PKPAAVGIL8L6MCSVQK90V1GLORV22PQI6H13PIGQL21E0HFAF0====" \
--data-urlencode "code_verifier=2ENOENOGA0USUNPROMSUD9U64P604R2LVOVDG5SEL7EIGA5SL3TC2BQN0MJVVG8S" \
--data-urlencode "redirect_uri=http://localhost/oauth2callback"

and you get back

{
  "access_token":"C45B3IG4CAHUDG1TAN03USAA55KDS2NA30VQIFLTN9FNMICPA2DG",
  "expires_in":1800,
  "refresh_token":"15IA4J2O0NCH08D6URPK6O3S38G3B7RFBFOPJVV20MNJNHJ11JIG",
  "token_type":"Bearer"
}

the autohost flow

I configured one client_id/client_secret pair that can be used to retrieve an auth token:

curl -ivv "https://tachyon.geekingfrog.com:4567/oauth/token" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=207966ee-6997-433f-a1fa-28da8f3b754e" \
--data-urlencode "client_secret=NENEGFN5MNNA6TH34P53JE88STHA5RCQJ06RKTG5HN92UMAOQ72G"

and this gives the same type of response.

Testing

There are automated tests under 2 location, one for the context and one for the controllers:

mix test test/teiserver/o_auth/
mix test test/teiserver_web/controllers/o_auth

Out of (OAuth) Scope for this PR

  • Currently the only thing you can do with an auth token is refresh it. I'll implement the basic websocket connection later.
  • Any CRUD operations or interface to manage applications and client credentials. For now, manually inserting things in the DB will have to do.

@p2004a
Copy link
Contributor

p2004a commented Jun 16, 2024

🎉 I will review it for sure (don't have perms to assign myself as reviewer)

issuer: base,
authorization_endpoint: base <> ~p"/oauth/authorize",
token_endpoint: base <> ~p"/oauth/token",
token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.rfc-editor.org/rfc/rfc6749.html#section-2.3.1

The authorization server MUST support the HTTP Basic authentication scheme for authenticating clients that were issued a client password.

I interpret that as client_secret_basic effectively being required on this list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added support for basic auth in 19bc2c2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to not be resolved, I've tested with curl and I get missing client_id when passing client_id via Basic auth:

curl --user generic_lobby: -d grant_type=authorization_code -s -X POST http://localhost:4000/oauth/token

@geekingfrog geekingfrog force-pushed the tachyon-oauth branch 2 times, most recently from 13f0d02 to f08f739 Compare June 27, 2024 20:45
@geekingfrog geekingfrog marked this pull request as ready for review June 27, 2024 20:47
@geekingfrog geekingfrog force-pushed the tachyon-oauth branch 5 times, most recently from d973582 to aff1262 Compare July 10, 2024 19:55
Copy link
Contributor

@p2004a p2004a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the authorization screen when user is not logged in? Will it first ask to login, then forward directly to auth screen?

lib/teiserver/o_auth/schemas/token.ex Outdated Show resolved Hide resolved
lib/teiserver_web/controllers/o_auth/code_controller.ex Outdated Show resolved Hide resolved
lib/teiserver/o_auth.ex Outdated Show resolved Hide resolved
@geekingfrog geekingfrog force-pushed the tachyon-oauth branch 3 times, most recently from 3693542 to 3514c24 Compare July 16, 2024 19:07
@geekingfrog
Copy link
Contributor Author

How does it work with the authorization screen when user is not logged in? Will it first ask to login, then forward directly to auth screen?

Yes exactly. It'll preserve any potential querystring (so state stuff is there).

@geekingfrog
Copy link
Contributor Author

geekingfrog commented Jul 16, 2024

Thanks for the review @p2004a , I believe I've addressed all your comments. Ping me on discord if you want me to restart the test server on my (tiny) vps, I've turned it off to save a bit of resources.

Setup schema, a query on primary key and a test. No UI or write ops yet.
Don't put a `?` symbol if there's no querystring
Screen for the user to grant access to the given client and then
redirect with a freshly generated authorization code.
@geekingfrog geekingfrog force-pushed the tachyon-oauth branch 2 times, most recently from 7b2a579 to 5ddcc77 Compare July 28, 2024 17:32
@p2004a
Copy link
Contributor

p2004a commented Jul 29, 2024

I've done a bunch of testing today, the biggest problem is lack of support for mandatory HTTP Basic authentication scheme for all but client credentials grant, that's a blocker.

A bunch of other things I've noticed that I don't think are hard blockers:

  1. state is always passed to redirect uri, even when not set in the initial request at all.

    That's on the edge of spec correctness AFAIU.

  2. Refresh token shouldn't be generated for client credentials grant: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3

    But shouldn't != must not.

  3. invalid scopes not verified properly, overall I'm not sure how well the scopes are handled. E.g. sending curl -d grant_type=refresh_token -d refresh_token=N0G2J8BN1TTFA5HGGLGPMG1PR2V9EJ2LFT8GGO8G8MAR5K1BG6JG -d scope=asdasd -s -X POST http://localhost:4000/oauth/token doesn't error out.

    That is incorrect per spec, but unlikely any clients at the moment will run into it.

  4. The error codes are very vague, invalid_request is returned very often when a different error code like invalid_grant should be user or even invalid_client when bad credentials are passed in client credentials grant. error_description is often not filled in with something helpful.

    That just makes it harder to debug interaction with server for clients.


One more question: how are old codes/tokens etc cleaned up from db?

@p2004a
Copy link
Contributor

p2004a commented Jul 29, 2024

One more question more on design side: I'm not sure I understand the relationship between autohost and application, why they are tied, can you elaborate on that design choice?

@geekingfrog
Copy link
Contributor Author

geekingfrog commented Aug 4, 2024

One more question more on design side: I'm not sure I understand the relationship between autohost and application, why they are tied, can you elaborate on that design choice?

I don't think an autohost is tied to an application. The schema only specify an id and a name (and some timestamps).
The tokens and client id/secret are the thing tied to an application, for the authorized scopes and whatnot.

About cleaning up old codes and tokens, I'll create an scheduled task to handle that later.

For point 3: I wasn't aware you could change the scopes when refreshing a token. The scope param is ignored when refreshing, you get the same scope as the original token, regardless of the request.

4: I'll improve that. I left them deliberately vague "for security", but it's more a nuisance than anything.

@p2004a
Copy link
Contributor

p2004a commented Aug 4, 2024

I don't think an autohost is tied to an application. The schema only specify an id and a name (and some timestamps).
The tokens and client id/secret are the thing tied to an application, for the authorized scopes and whatnot.

Ah, application <- client credentials -> autohosts. Looks like I got confused about the autohosts table purpose. Currently it's rather empty, but maybe there will be use cases in the future.

For point 3: I wasn't aware you could change the scopes when refreshing a token. The scope param is ignored when refreshing, you get the same scope as the original token, regardless of the request.

So from https://datatracker.ietf.org/doc/html/rfc6749#section-6

scope
         OPTIONAL.  The scope of the access request as described by
         Section 3.3.  The requested scope MUST NOT include any scope
         not originally granted by the resource owner, and if omitted is
         treated as equal to the scope originally granted by the
         resource owner.

and later

   The authorization server MAY issue a new refresh token (...) If a
   new refresh token is issued, the refresh token scope MUST be
   identical to that of the refresh token included by the client in the
   request.

so effectively, you could theoretically reduce the scope of access token returned by endpoint, by specifying a subset of specified scopes.

Quite weird, but ok.

@geekingfrog geekingfrog force-pushed the tachyon-oauth branch 2 times, most recently from 669c46d to bb132bd Compare August 5, 2024 19:06
@geekingfrog
Copy link
Contributor Author

Alright, I think I've got everything now, the last 6 commits should do the job.

I've added some basic check if you provide a scope parameter when refreshing a token, but it's incomplete. Since the only scope supported right now is tachyon.lobby it doesn't really make sense to support more right now.

Copy link
Contributor

@p2004a p2004a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work! Thank you!

@L-e-x-o-n Please PTAL and merge if all looks good from elixir side.

Copy link
Collaborator

@L-e-x-o-n L-e-x-o-n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all really well done.
I just have a few questions, but I think it's ready to be merged.

config/runtime.exs Show resolved Hide resolved
lib/teiserver/o_auth.ex Show resolved Hide resolved
owner_id: user_id,
application_id: attrs.id,
scopes: attrs.scopes,
expires_at: Timex.add(now, Timex.Duration.from_minutes(5)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be configurable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@p2004a do you have any opinion on this? I'm personally neutral, it's easy enough to change in the code, but does require a recompilation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion there is no need. The code is supposed to be used immediately by a program, not in any way by humans, so 5m is very generous and I don't think we would need to change it in a hurry. Shortening it also doesn't improve anything from security side.

value: Base.hex_encode32(:crypto.strong_rand_bytes(32), padding: false),
application_id: application.id,
scopes: application.scopes,
expires_at: Timex.add(now, Timex.Duration.from_minutes(30)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be configurable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one could be, also the refresh token, but IMHO we don't need to add it already in this PR. I think it is unlikely to change too and can't think about a good reason to have it different across instances.. maaaybe in dev setup for testing clients code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say ship it. If we need to tweak that for dev code, that's easy, and this PR is already massive enough.
Worst case, ping me and I can setup a teiserver on my vps.

lib/teiserver/o_auth.ex Outdated Show resolved Hide resolved
lib/teiserver/o_auth.ex Outdated Show resolved Hide resolved
lib/teiserver/o_auth.ex Outdated Show resolved Hide resolved
Because it's more obnoxious than anything to just get "invalid_request"
when something goes wrong without any indication about the problem.
As per https://www.rfc-editor.org/rfc/rfc6749.html#section-2.3

> The client MUST NOT use more than one authentication method
> in each request.
This is incomplete but until teiserver supports more than one scope it
is not really possible to test that. It also doesn't really make sense.
@L-e-x-o-n L-e-x-o-n merged commit 925a212 into beyond-all-reason:master Aug 8, 2024
3 checks passed
@geekingfrog geekingfrog deleted the tachyon-oauth branch August 9, 2024 06:25
@geekingfrog geekingfrog mentioned this pull request Aug 19, 2024
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.

3 participants