-
-
Notifications
You must be signed in to change notification settings - Fork 129
feat(auth): adds basic oauth support #1461
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
base: main
Are you sure you want to change the base?
Conversation
c1aa75a
to
acf1b89
Compare
I very much agree with the philosophy: we should provide only primitives with almost no opinion regarding the final implementation |
Great work on this implementation! This OAuth feature should support provider-specific security checks to strengthen protection against token replay, tampering, and other attacks.
It would be great to build on this approach by allowing flexible, provider-specific customization as needed! |
@shaffe-fr Thanks for those great ideas 🎉 Is the I think all security methods you mentioned can be done depending on the provider, but I'll look into them to see if I can implement some of these to be easier to use or abstracted in requests like the FYI, I've looked into Socialite implementation to check important things and DX |
Yes, that’s exactly what I meant—your mention of When calling Regarding the security checks, I agree they should be handled within the provider implementation. I was thinking that adding an optional method to encourage these validations could be helpful. In the Tempest style, this might look like using a dedicated request or mapper class to encapsulate those checks cleanly. From my experience implementing this for a client, session management can feel a bit clunky—storing stuff in session inside a |
I haven't looked through this in depth yet.
I do wonder why not? I've been using the phpleaguea wrapper and it's really intuitive + has support for so many providers. Why start from scratch in this case? |
@brendt It was to challenge the |
I agree that we should use the PHP League's OAuth2 client. As Brent said, there are many providers available. We should wrap the API to make it Tempest-y, and maybe wrap the provider API while letting the ability to use PHP League wrappers (like we do for email and storage providers). The only issue I can foresee is with the HTTP client under the hood, but I assume they provide a way to specify a PSR-compliant one |
I'll check those details ASAP 😄 |
@innocenzi I've delve into the PHPLeague client, IMO there are some problems for us. Make the DX Tempest-y means ( among other things ) that we want named parameters, I can't figure out how to do something like that for each custom providers if we don't wrap them one by one. If we look at the given providers, we can see that there are 5 officials there https://oauth2-client.thephpleague.com/providers/league/ and a lot of third-party providers https://oauth2-client.thephpleague.com/providers/thirdparty/ |
Just wanted to pitch in here: I'll come back to this PR when 2.0 has been tagged. |
Sets up the Github Single Sign-On (SSO) authentication flow. The changes introduce dependency injection for HttpClient, move configuration to the constructor, and generate the authorization URL based on scopes, client ID, and redirect URI. This streamlines the authentication process.
Updates the codebase to use a more generic OAuth terminology instead of the specific SSO term. This change involves renaming several files and namespaces to reflect the broader OAuth concept. This allows for more flexibility and easier integration with different OAuth providers.
Moves common OAuth logic into a trait for reusability. Adds support for "expires_in" and "refresh_token" in the AccessToken data object. Generalizes authorization parameter handling in OAuthManager. Introduces Google OAuth provider. This change aims to provide a more flexible and extensible OAuth implementation.
Adds custom exceptions for OAuth flows to better handle invalid states and codes. Stores the OAuth state in the session to validate the response and prevent CSRF attacks.
Simplifies the OAuth provider interface by removing the "Contract" suffix, promoting consistency and readability. This change streamlines the codebase by using a more straightforward naming convention for the OAuth provider interface.
Ensures the session flash key for the OAuth state is configurable. This change introduces a `stateSessionSlug` property, allowing customization of the session key used to store the OAuth state, improving flexibility and avoiding potential naming conflicts.
Makes the `stateSessionSlug` property public to allow easier access and modification when needed.
Adds support for built-in OAuth providers like Github and Google. This change introduces: - A `BuiltInOAuthProvider` enum to manage supported providers. - A dynamic initializer for creating `OAuth2Provider` instances based on the selected provider, using environment variables for configuration. - An `OAuthManagerInitializer` for resolving OAuthManager dependencies. This approach simplifies OAuth integration by providing pre-configured providers and ensuring that necessary environment variables are set correctly, throwing an exception if something is missing.
6f1e089
to
c9361cd
Compare
SO, I've updated the PR to use PHPLeague by wrapping it to get benefits of it internally but having the Tempest DX Now, here's how it looks to perform OAuth now : final class OAuthController
{
#[Get('/auth/google')]
public function redirect(
#[Tag('from_env_variables')] GoogleProvider $provider // There's a built-in initializer to load providers from env
): Response
{
return new Redirect(
$provider->generateAuthorizationUrl()
);
}
#[Get('/auth/google/callback')]
public function callback(
#[Tag('from_env_variables')] GoogleProvider $provider,
Request $request,
Authenticator $authenticator,
Session $session
): Response
{
if ( ($requestState = $request->get('state')) === null ) {
throw new InvalidStateException('Missing state parameter in request');
}
if ( ($sessionState = $session->consume($provider->stateSessionSlug)) === null ) {
throw new InvalidStateException('Missing state in session');
}
if ( $requestState !== $sessionState ) {
throw new InvalidStateException('State mismatch between request and session');
}
$accessToken = $provider->generateAccessToken(
code: $request->get('code') ?? throw new InvalidCodeException('Missing code parameter in request'),
);
$userData = $provider->fetchUserDataFromToken($accessToken);
$user = User::findOrNew(
find: [
'email' => $userData->email
],
update: [
'email' => $userData->email,
'password' => str()->random(40)->toString() // Random password as OAuth users don't need a password
]
);
$user->save();
$authenticator->authenticate($user);
return new Redirect('/auth/dashboard');
}
} If you want to switch provider you must only use different provider in route params like so : #[Get('/auth/github')]
public function redirect(
#[Tag('from_env_variables')] GithubProvider $provider
): Response You can also configure the provider from user input or DB data or something else : #[Get('/auth/google')]
public function redirect(
GoogleProvider $provider,
Request $request,
): Response
{
$provider->configure(
clientId: $request->get('client_id'),
clientSecret: $request->get('client_secret'),
redirectUri: $request->get('redirect_uri'),
);
return new Redirect(
$provider->generateAuthorizationUrl()
);
} Finally, You also can use a #[Get('/auth/google')]
public function redirect(
GenericProvider $provider,
): Response
{
$provider->configure(
clientId: env('GOOGLE_CLIENT_ID'),
clientSecret: env('GOOGLE_CLIENT_SECRET'),
redirectUri: env('GOOGLE_REDIRECT_URI'),
defaultScopes: ['openid', 'profile', 'email'],
authorizeEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
accessTokenEndpoint: 'https://accounts.google.com/o/oauth2/v2/token',
userDataEndpoint: 'https://openidconnect.googleapis.com/v1/userinfo',
);
return new Redirect(
$provider->generateAuthorizationUrl()
);
} |
Hi @gturpin-dev, Did you had something in mind? |
👋 @shaffe-fr I have to try non-built-in providers with the new implementation. If possible, I'll try to avoid two interfaces to distinguish between a classic provider and a built-in one ( this is why I've done an enum to list them, but still don't 100% convinced by that btw ) |
Hey 👋
Wanted to give a first-party implementation of OAuth support ( #1351 ) as discussed in discord
So it's different from Authentik as it is first-party PR and it does not uses phpleague wrapper
HttpClient
provided by the container to make calls to providersIt also differ because I assume having a database implementation is opinionated so I think we should provide tools and docs to help doing this with less friction as possible with our auth package for example
But we shouldn't assume that a specific field on database is here to store refresh tokens or something else
That said, It's a draft PR as this hasn't been tested yet with a lot of providers, some adjustments may be needed.
Also, there aren't any tests yet, I don't know well how to test this whole workflow, as the majority of the work include userland things, plus I wouldn't go into it before discussing about the feature before
So, for a Tempest app to use this implementation, it needs a user model ( I've tested it with builtin auth installer ).
Here's a controller that handle all parts of the flow :
To summarize the workflow
We first call
/auth/google
which will redirect to the given provider ( all built-in providers are handled byInitializers
to make it easier to add an unsupported one, I have an example that can be added in the docs )The provider then will call our
/auth/google/callback
route which will generate the access token using the code/stateThen we will fetch the user data and create it if not exist yet and login him
After that I redirect to an example dashboard route that display the current user
What's missing now :
If you like the approach I can dig the above points