goauthentik / authentik

The authentication glue you need.
https://goauthentik.io
Other
13.08k stars 870 forks source link

How to setup Authentik for OAuth2 Password Grant? #5860

Open yceruto opened 1 year ago

yceruto commented 1 year ago

Describe your question/ We are trying to use Authentik as our own identity provider inside a Microservice architecture to login users through OAuth2 Password Grant. Basically, we want to implement this flow: https://auth0.com/docs/get-started/authentication-and-authorization-flow/resource-owner-password-flow.

We found in the documentation that it's not listed as grant type https://goauthentik.io/docs/providers/oauth2/ but inside https://goauthentik.io/docs/providers/oauth2/client_credentials there is a note about password grant saying:

Note that authentik does treat a grant type of password the same as client_credentials to support applications 
which rely on a password grant.

Further, checking the OpenID endpoint /application/o/{appName}/.well-known/openid-configuration we see that the password grant is supported:

{
    "grant_types_supported": [
        "authorization_code",
        "refresh_token",
        "implicit",
        "client_credentials",
        "password",
        "urn:ietf:params:oauth:grant-type:device_code"
    ],
}

According to that, we've tried to send requests with many alternatives, including scope, response_type, etc, without success:

curl --request POST \
  --url 'https://{domain}/application/o/token/' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=password \
  --data 'username={username}' \
  --data 'password={password}' \
  --data 'client_id={clientId}' \
  --data 'client_secret={clientSecret}'

It always fails saying:

{
    "error": "invalid_grant",
    "error_description": "The provided authorization grant or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client"
}

We appreciate it any advice about this topic or any confirmation that the password grant is not supported. Thanks!

Version and Deployment (please complete the following information):

BeryJu commented 1 year ago

the password grant (since it is handled internally like the client_credentials) grant only works with Tokens of the kind of APP_PASSWORD

yceruto commented 1 year ago

@BeryJu does that mean we cannot authenticate on behave of a user?

From what we tried, the client_credentials work properly with the service account + token but we are not able to access to protected endpoint where a user resource is required, e.g. /core/users/me, and that's expected as it's a machine-to-machine flow.

Thanks for your quick answer.

grebois commented 1 year ago

@BeryJu @rissson What we are trying to accomplish is that a user authenticates using only username and password, as specified in the RFC 6749 as Resource Owner Password Credentials Grant it looks like this:

ROP_Grant

I took that from Auth0 because it was the simplest one I could find but in the end, the Resource Owner Password Flow is always the same:

Resource-Owner-Password-Credentials-Grant-Flow

Is this something that could be done using Authentik?

minz1 commented 1 year ago

I'm interested in hearing more about this too. I've been reading documentation about this kind of use-case and I've been having difficulty implementing this.

thechubbypanda commented 1 year ago

Currently also having issues with this, working on dms and can't get grant password working with app passwords.

aetmezgu commented 1 year ago

hello, same error for me, is there any workaround to make the password flow authentication work with a curl request on goauthentik ?

dweebertwt commented 3 months ago

same here for Jamf Connect

dweebertwt commented 3 months ago

@fheisler Maybe this is related to #6139 ? In my case Jamf Connect tries to validate username/password of user by sending it to the token endpoint with "password" as grant_type. Unfortunately authentik only reponds with HTTP 400.

so, basically, we need this to function:

curl --request POST \
  --url 'https://authentik.url.local/application/o/token/' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=password \
  --data username=user-login \
  --data password=user-pw \
  --data 'client_id=client-id' \
  --data 'client_secret=client-secret' \
  --data 'scope=openid profile read:sample'
thunoldolsen commented 3 months ago

Agree. Im having real difficulties understanding how to solve it other ways… is it possible to create a flow for this?

rissson commented 3 months ago

--data password=user-pw \ this should be an app password instead of the user's password (see https://github.com/goauthentik/authentik/issues/5860#issuecomment-1577081591)

DanielWeeber commented 3 months ago

Yeah, but with esp Jamf Connect the user password gets transferred, as the user types in his password. This is the one which needs to be verified, not an app password. Like the others also mentioned after the comment #5860

spacemule commented 1 month ago

This is really not clear in the documentation what an app_password is (i.e. how it differs from a token), if there's a way for a client to request one except through the web interface, or what its purpose is altogether. I'm trying to set up a similar setup to many here:

User gets invited to create an account, creates it, and creates an API Key. This API key is used to authenticate with authentik which issues JWTs for the user.

There doesn't seem to be a clear distinction between tokens and app passwords. The web interface and API call them both tokens. All I've found is that the app passwords work with the /application/o/token/ endpoint, and the tokens don't.

EDIT: Just figured this out for anyone else confused. The admin GUI and terraform docs shed some light: Tokens are just for API access and app passwords are for flow authentication.

maksym-connect commented 1 month ago

I figured out how to fix this problem. The main reason why we have this problem is because someone merged GRANT_TYPE_CLIENT_CREDENTIALS and GRANT_TYPE_PASSWORD a long time ago.

https://github.com/goauthentik/authentik/blob/b301048a272aa76756d9d0d13d0b8b8fedeb0cfe/authentik/providers/oauth2/views/token.py#L163-L167

https://github.com/goauthentik/authentik/blob/b301048a272aa76756d9d0d13d0b8b8fedeb0cfe/authentik/providers/oauth2/views/token.py#L531-L533

Digging deeper I found out. The error occurs only because the user and his APP_PASSWORD are checked. Logically, a regular user does not have this password.

https://github.com/goauthentik/authentik/blob/b301048a272aa76756d9d0d13d0b8b8fedeb0cfe/authentik/providers/oauth2/views/token.py#L311-L339

But we can add a simple check and everything will start working like clockwork. If the usertype is a SERVICE_ACCOUNT, check his APP_PASSWORD, if the user is regular, check his password in the database. But this is still not the correct behavior.

    def __post_init_client_credentials_creds(
        self, request: HttpRequest, username: str, password: str
    ):
        # Authenticate user based on credentials
        user = User.objects.filter(username=username).first()
        if not user:
            raise TokenError("invalid_grant")
        if user.type == UserTypes.SERVICE_ACCOUNT:
            token: Token = Token.filter_not_expired(
                key=password, intent=TokenIntents.INTENT_APP_PASSWORD
            ).first()
            if not token or token.user.uid != user.uid:
                raise TokenError("invalid_grant")
        else:
            if not user.check_password(password):
                raise TokenError("invalid_grant")
        self.user = user
        # Authorize user access
        app = Application.objects.filter(provider=self.provider).first()
        if not app or not app.provider:
            raise TokenError("invalid_grant")
        self.__check_policy_access(app, request)
        Event.new(
            action=EventAction.LOGIN,
            **{
                PLAN_CONTEXT_METHOD: "token",
                PLAN_CONTEXT_METHOD_ARGS: {
                    "identifier": token.identifier if token else {},
                },
                PLAN_CONTEXT_APPLICATION: app,
            },
        ).from_http(request, user=user)
maksym-connect commented 1 month ago

How it works now, grant_type "password" and "client_credentials" works the same!!!:

OK

curl --location 'https://auth.example.com/application/o/token/' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'username=your_service_account' \
--data-urlencode 'password=your_service_account_APP_PASSWORD'

OK

curl --location 'https://auth.example.com/application/o/token/' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'client_secret=your_client_secret'

OK

curl --location 'https://auth.example.com/application/o/token/' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'client_secret=your_client_secret'

OK

curl --location 'https://auth.example.com/application/o/token/' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'username=your_service_account' \
--data-urlencode 'password=your_service_account_APP_PASSWORD'

Ideally, these two flows should be separated. When code validate client_id and client_secret it returns the client_credentials token. If client_secret is invalid or empty it tries to check service account and APP_PASSWORD.

After my patch, both flows still don't work correctly. It turns out that we can log out a user without client_secret, regardless of grant_type.

Case 1:

curl --location 'https://auth.example.com/application/o/token/' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password or client_credentials' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'client_secret=your_client_secret'
// will be ignored
--data-urlencode 'username=username' \
--data-urlencode 'password=password'

Case 2:

curl --location 'https://auth.example.com/application/o/token/' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password or client_credentials' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'username=username' \
--data-urlencode 'password=password'