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

Add PKCE to user authentication #189

Closed
felix-hilden opened this issue Jun 29, 2020 · 7 comments · Fixed by #193
Closed

Add PKCE to user authentication #189

felix-hilden opened this issue Jun 29, 2020 · 7 comments · Fixed by #193
Assignees
Labels
enhancement New feature or improvement
Milestone

Comments

@felix-hilden
Copy link
Owner

felix-hilden commented Jun 29, 2020

Spotify has announced support for the PKCE authentication method (guide). We should implement it. It seems that Spotify has documented it as optional, but reading the RFC, it is presented as an extension that should be used instead of the original one. So I think we won't provide any parameters to switch it off.

We should also consider the Implicit Grant flow and could it be useful. I think Spotify's disclaimer of it being a JS-only method put me off until now when I gave it more thought. It seems that the need for this does not exist, as it cannot be used on a server, and PKCE is the preferred method for authentication that does not use a client secret.

Let's also add better documentation pulling all these authentication methods together.

@felix-hilden felix-hilden added the enhancement New feature or improvement label Jun 29, 2020
@felix-hilden
Copy link
Owner Author

felix-hilden commented Jun 29, 2020

This raises a problem where each generated code challenge must be matched with a verifier it was generated with. I see these approaches.

  • Store the previous verifier as class state. This has the obvious problem of not being able to handle parallel authentications.
  • Send the verifier as state. It would need to be sent as plain text, making the whole enhancement pointless.
  • Send a reference to a verifier as state. We could generate a sufficiently long random string to send as additional state. These strings would then map to verifiers and be deleted on use. In practice we would need to append state in case the user specifies it. But users should also be able to tell which verifier an authorisation url was constructed with.

This needs to be thought out. Not having an ability to use whatever instance of Credentials for any request is a bit limiting and appending state seems hacky. Using some object to signify each authentication instance could be useful. But it would be a breaking change. Let's see. Using objects for each authentication could look like this:

class Auth:
    client_id: str
    client_secret: str = None
    redirect_uri: str = None

    def request_client_token() -> Token
    def make_user_flow(state: str = None, verifier_bytes: int = 32) -> AuthorisationCodeFlow
    # Verifier bytes in minimum 32, max result clipped to 128 characters

class AuthorisationCodeFlow:
    client_id: str
    redirect_uri: str
    code_verifier: str
    challenge_method: str

    def authorisation_url(scope, show_dialog) -> str
    def request_token(code: str, state_check: str = None) -> str

I'm not sure about the names, but that's the gist of what I have in mind. The usage could look like this:

auth = tk.Auth(client_id, client_secret, redirect_uri)
flow = auth.make_user_flow(state)
url = flow.authorisation_url(scope, show_dialog)
code, state_check = ...  # Redirect and get code and state
token = flow.request_token(code, state_check)

It's pretty similar, with one added line instantiating the flow. This is at least better than using the state or having some hacky previous value for verifiers. Additionally, it would allow for automatically checking state, which should also require state_check above if it was specified in making the flow. We could also always generate a state if it is not specified.

Maybe we won't implement this in Tekore 2 just yet. It would introduce too many problems without these changes.

@felix-hilden felix-hilden added the change Change behaviour of an existing component label Jun 30, 2020
@felix-hilden felix-hilden changed the title Expand authentication Add PKCE to user authentication Jun 30, 2020
@felix-hilden felix-hilden added this to the v3.0.0 milestone Jun 30, 2020
@felix-hilden
Copy link
Owner Author

Of course this can and should be implemented as an enhancement and deprecation of the original functionality, provided that the names don't clash.

@felix-hilden
Copy link
Owner Author

felix-hilden commented Jun 30, 2020

With the non-inclusion of Implicit Grant Flow, having one flow object could also be overkill. PKCE changes the refreshing of tokens too, so the refresh method needs to be changed anyway. Not using flow objects would allow for simpler changes.

cred = tk.Credentials(client_id, client_secret, redirect_uri)
url, verifier = cred.user_authorisation_url(scope, state, verifier_bytes)
code, state_check = ...  # Redirect and get code and state
if state == state_check:
    token = cred.request_user_token(code, verifier)
else:
    raise SomeError('Important message.')

This wouldn't allow for automatically generating state, but a utility with good examples might be sufficient for that.

NOTE

The new refreshing method does not work for tokens generated with the previous method. Users should be informed of this.

@felix-hilden felix-hilden self-assigned this Jun 30, 2020
@felix-hilden felix-hilden mentioned this issue Aug 15, 2020
10 tasks
@felix-hilden
Copy link
Owner Author

felix-hilden commented Aug 24, 2020

Okay, so the Spotify authorisation guide states at the very end of its PKCE section, that its refresh token may be used only once to get a new access token, after which it is invalidated. So this may not be a replacement to the regular user auth flow. The refresh tokens can be chained and tokens used for their full duration, but this makes testing a lot harder. I opened a discussion on Spotify forums.

@felix-hilden felix-hilden added change Change behaviour of an existing component and removed change Change behaviour of an existing component labels Aug 24, 2020
@felix-hilden
Copy link
Owner Author

felix-hilden commented Aug 27, 2020

Given no quick response from Spotify, we should implement PKCE as an option. So we ought to choose some names.

  • 1/2 without PKCE: user_authorisation_url
  • 2/2 without PKCE: request_user_token
  • Refresh without PKCE: refresh_user_token
  • 1/2 with PKCE: pkce_user_authorisation
  • 2/2 with PKCE: request_pkce_token
  • Refresh with PKCE: refresh_pkce_token

These are reasonably similar while also highlighting the fact that PKCE introduces special semantics.

Sadly this addition makes it impossible to distinguish between ordinary user refresh tokens and PKCE versions. This affects two things: Credentials.refresh and configuration. Configuration-wise, as the name of the refresh token variable can be configured, they can be saved and loaded even from the same file. But this kind of use shouldn't be expected, because a large part of the security is lost if ordinary tokens are also in use.

I would lean towards removing the type-agnostic refresh method, if the change didn't affect self-refreshing tokens too. They need a way to tell if they were created with PKCE or not, so let's introduce some boolean to all tokens while were at it.

@felix-hilden felix-hilden removed the change Change behaviour of an existing component label Sep 2, 2020
@felix-hilden
Copy link
Owner Author

In the end, this doesn't require changes. We can naturally use the new boolean for tokens in deciding how to refresh them.

@felix-hilden
Copy link
Owner Author

Closed in #193

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or improvement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant