Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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 custom scheme handler / intent handler to accomodate single-sign-on via browser #28

Closed
strazto opened this issue Jul 25, 2022 · 8 comments
Labels
enhancement New feature or request feature-request

Comments

@strazto
Copy link

strazto commented Jul 25, 2022

Request

Basically, I'd like to be able to open a link with a custom schema to the app that supplies:

- serverUrl
- userId
- accessToken

To an intent handler (or whatever we call it in ios), that stores that information in the web client to initiate a session, and skips the login.

This would intend to accomodate single-sign-on via plugin, eg the popular

https://github.com/9p4/jellyfin-plugin-sso

Here is a demonstration of the current flow on the android client (behaves similarly)

Screen_Recording_20220717-200505_Firefox.Beta.mp4

Take note that although we can get a via single sign on, the new session is in the browser, rather than the jellyfin app.

From a technical perspective, this is not too hard to implement, both Android and iOS have APIs for using custom links open apps with some kind of data payload https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app

Background

I am one of the maintainers of a plugin that provides single-sign-on to jellyfin servers

https://github.com/9p4/jellyfin-plugin-sso

The plugin is now at a stage of development where its functionality is quite mature, but its main limitation is that it only supports the web-client.

Flows

Assume the jellyfin server lives at https://myjellyfin.com

The plugin can serve a page that initiates the SSO flow (either oauth or saml, I'm mainly going to talk about oauth), this page is at https://myjellyfin.com/SSO/OID/p/<provider_name>.

The plugin also serves a callback page for completing SSO flows, eg https://myjellyfin.com/SSO/OID/r/<provider_name>.

For example, you might use Google as your provider, so you'd use https://myjellyfin.com/SSO/OID/p/google, and https://myjellyfin.com/SSO/OID/r/google

On the web-client, the SSO flow from login-page to signed in dashboard is straightforward.

User perspective (Web client):

  1. On the login page, not signed in
  2. Click on the link to "Sign in via SSO"
    • Get redirected to the sign in-page for your SSO provider (eg, google)
    • complete the flow on your SSO provider (eg, confirm sign-in, or select your account)
    • get redirected back to jellyfin, and you're signed in

Actual flow (Web client)

  1. User on the login page, not signed in
  2. User click on the link to "Sign in via SSO"
  3. (OAuth magic) This takes user to a page (https://myjellyfin.com/SSO/OID/p/google with a javascript client that initiates an OAuth flow
    1. The js client redirects the user to https://accounts.google.com/o/oauth2/v2/auth
      • The user completes the prompt on google
    2. Google redirects to https://myjellyfin.com/SSO/OID/r/google with the oauth state required
  4. On the redirect page, a js client uses the state given by google to confirm a valid session with the jellyfin server
    • The server gives the client a user ID + authorization token
    • THe client stores this in localStorage, with enough information for the browser to now have a valid, logged in session
    • The client redirects back to the homepage, (https://myjellyfin.com) and is now logged in

This flow works reliably. An important note, however, is that a lot of the oauth client-handling is done using javascript clients in web pages server directly by the plugin.

Limitations

Because the above flow relies on a browser oauth client, it effectively establishes a valid session within the browser

https://github.com/9p4/jellyfin-plugin-sso/blob/fad5a62e07e44c84bc94d5962b8b79e80c102ee8/SSO-Auth/WebResponse.cs#L449-L458

    var responseJson = JSON.parse(response);
    var userId = 'user-' + responseJson['User']['Id'] + '-' + responseJson['User']['ServerId'];
    responseJson['User']['EnableAutoLogin'] = true;
    localStorage.setItem(userId, JSON.stringify(responseJson['User']));
    var jfCreds = JSON.parse(localStorage.getItem('jellyfin_credentials'));
    jfCreds['Servers'][0]['AccessToken'] = responseJson['AccessToken'];
    jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
    localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
    localStorage.setItem('enableAutoLogin', 'true');
    window.location.replace('" + baseUrl + @"');

It literally stores all the state the web-app needs in order to resume a session.

The problem is, that we can't use this to log-into the android or ios app

Additional context

Discussion migrated from Matrix - Pretty long but included for posterity

Matthew Strasiotto: (July 6ish)

question -
Is it at all possible to add an intent to the jf android app that signs onto a user account using a token generated from elsewhere?
i'm reading about SSO for android, and how to add SSO clients to android apps
https://github.com/openid/AppAuth-Android

i'm a contributor to https://github.com/9p4/jellyfin-plugin-sso
the actual oauth client works on jellyfin-web, & i'd like to get it to work for the android client -
The best way to do that is probably something along the lines of AppAuth-Android / maybe intent filters

in reality, as long as we can trigger some kind of redirect back to the jellyfin app with enough data from the authenticated web-session to get a user logged into the app, there isn't a tonne of work

...

Matthew Strasiottio:

mcarlton
Matthew Strasiotto

would quick connect accomplish what you're looking for? i'm not sure if it can be used in the android app as a client side yet

Quick connect is good, and a good workaround for the use-case where people's primary authentication is SSO
It does work on the android app, but the limitation is that the current SSO flow my plugin gives will basically Auth a session in the browser, and a user would need to

  1. Initiate browser SSO
  2. Swap back to jellyfin, initiate quick connect
  3. Swap back to browser, complete quick connect flow

Pretty clunky, good for Auth across devices tho

Matthew Strasiotto July 17:

I'd like to support the SSO plugin i contribute to in jellyfin-android & jellyfin-expo
I've heard from maintainers that they're not interested in supporting SSO in their apps at this stage as it would require each client implement it.
I'd like to get around that with fully browser/web-app based implementation that's handled by the plugin, which basically handles the flow, gets the userID + access token

The browser client would then open a custom scheme link that passes the server ID, user ID & access token back to the app, and triggers a "sign on" intent

for either case, the jellyfin-android + jellyfin-expo app would only have to handle an intent being given with fully-formed credentials, and everything else could defer back to their web wrappers.

Before i commence work on this, I'm curious if maintainers are open to considering PRs on either project (jf-android / jf-expo) that allow custom URL scheme handling for the scope of "given serverID, userID & accessToken, initate a session then commence main activity" so that my plugin can handle the rest from a native browser

image

image

image

July 21

https://matrix.to/#/!YOoxJKhsHoXZiIHyBG:matrix.org/$qn4tvt0GXVlDnBYIGgM0wCuO0C2SO2AAUyAI9xiQHlc?via=bonifacelabs.ca&via=t2bot.io&via=matrix.org

image

image

image

@strazto
Copy link
Author

strazto commented Jul 25, 2022

Hi - This is migrated from the Jf-dev matrix channel @thornbill @nielsvanvelzen

@strazto strazto changed the title Add custom schema handler / intent handler to accomodate single-sign-on via browser Add custom scheme handler / intent handler to accomodate single-sign-on via browser Jul 25, 2022
@nielsvanvelzen
Copy link
Member

Thanks, I've looked at some other apps (mainly Element) to see how they add a similar feature. One change I'd like to make is to use a temporary login token that needs to be exchanged by the Jellyfin app for an access token.

So you get a flow similar to this:

  1. User opens SSO app
  2. SSO app deals with signin
  3. SSO app creates an exchange token in the server, this token expires in 5 minutes
  4. SSO app redirects to jellyfin://connect?exchange_token=ABCDEF&server=SERVERURL
  5. Jellyfin app reacts to this URL and opens a sign in activity
  6. Sign in activity checks the server (calling public system info) to see if it exists and if the server version is compatible with the app
  7. Sign in activity exchanges token with an access_token (server also responds with UserDto) and stores this locally (at this point the token is revoked serverside and cannot be used again)
  8. User is signed in

Login implementation for Element: https://github.com/vector-im/element-android/blob/afaa89ad42e1803b53c4610169646fedef4c0f68/docs/signin.md#login-with-sso

The sign in activity in the app will likely show a popup to confirm if the user wants to sign in. Maybe an endpoint to introspect the login token can be used to add more details to this popup (name of the user to sign in with for example).

@nielsvanvelzen nielsvanvelzen added the enhancement New feature or request label Jul 25, 2022
@strazto
Copy link
Author

strazto commented Jul 26, 2022

Awesome, glad to hear you're on-board with the use case!

The sign in activity in the app will likely show a popup to confirm if the user wants to sign in.

Seems reasonable - it'd be nice to make this optional, just for UX, since it's one extra click/touch, though I don't really know how configuring that could work (the point of an explicit login-consent flow is to ensure the user wants this, so we can't trust the query string to toggle it for example)

One change I'd like to make is to use a temporary login token that needs to be exchanged by the Jellyfin app for an access token.

Yep, this occurred to me too- It might be slightly more complex to implement (And I think I'll have to touch other aspects of the architecture?) but I do understand why you're requesting this.

it's kind of like a "reverse quick-connect" - From an authenticated session, generate a one-time code & a client consumes that.

Do you anticipate that I'll have to make changes to jellyfin-server to accomodate the exchange-token API? I suspect I will.

I don't think this is a bad thing- This being a general feature of jellyfin-server is probably a good thing, and will likely service use-cases beyond my specific one (Once it's generally available, i might even be able to use it to simplify my plugin)

Projected Overall architectual scope

My suspicion is that I'll be exposing the following API endpoints:

(These are just sketches of what it might look like so i understand the components)

jellyfin-server:

  • REST endpoints
    • /getAccessToken?exchangeToken=...
      • returns accessToken etc
    • /generateExchangeToken
      • Requires auth, generates a token to create a new session
      • Maybe an actual REST api isn't necessary, idk
  • Plugin Accessible modules
    • something.generateExchangeToken(uid)

my plugin:

This is pretty straightforward from my end

  • Given auth'd session, call generateExchangeToken (via whichever API)
  • open org.jellyfin://connect?exchange_token=ABCDEF&server=SERVERURL

clients:

  • Handle org.jellyfin://connect?exchange_token=ABCDEF&server=SERVERURL
  • Call ${SERVERURL}/getAccessToken?excahnge_token=ABCDEF, get whatever I need from that
  • Use that for session

Versioning concern

Assuming we need to change the jellyfin server's API - which I think is likely:

for a client to use this functionality, they will need to call:

${SERVERURL}/getAccessToken?excahnge_token=ABCDEF

This will fail (404 probably) on jf server versions that predate this feature.

We have a few options to handle this version dependency -

  • Wait to surface this feature until there's a proper jellyfin-server release that implements this
  • If the request fails, gracefully degrade, either showing an error, or just return to normal login screen
    • Personally, I think this is a good option

@nielsvanvelzen
Copy link
Member

Do you anticipate that I'll have to make changes to jellyfin-server to accomodate the exchange-token API? I suspect I will.

Yeah definitely, the server should always have the API available and just do nothing when there is no plugin using it.

[api stuff]

The API changes seem fine to me, the generateExchangeToken operation might not be needed. The paths and exact parameters will probably change when the actual PR for the server is made. I think most requests will be POST instead of GET.

org.jellyfin://

I think just jellyfin:// should be fine here.

Versioning concern

Assuming all server changes are merged for 10.9 the official clients will only start supporting the feature in 10.9. They will likely read the public system info first (/system/info/public) and check the server version for support. When connect is not supported on the server it will show an error in the UI.

@nielsvanvelzen nielsvanvelzen transferred this issue from jellyfin/jellyfin-expo Jul 26, 2022
@nielsvanvelzen
Copy link
Member

(Moved to jellyfin-meta since this repository makes more sense for discussing organization wide changes)

@LexNastin

This comment was marked as off-topic.

@Sapd
Copy link

Sapd commented Nov 11, 2023

Thanks, I've looked at some other apps (mainly Element) to see how they add a similar feature. One change I'd like to make is to use a temporary login token that needs to be exchanged by the Jellyfin app for an access token.

So you get a flow similar to this:

  1. User opens SSO app
  2. SSO app deals with signin
  3. SSO app creates an exchange token in the server, this token expires in 5 minutes
  4. SSO app redirects to jellyfin://connect?exchange_token=ABCDEF&server=SERVERURL
  5. Jellyfin app reacts to this URL and opens a sign in activity
  6. Sign in activity checks the server (calling public system info) to see if it exists and if the server version is compatible with the app
  7. Sign in activity exchanges token with an access_token (server also responds with UserDto) and stores this locally (at this point the token is revoked serverside and cannot be used again)
  8. User is signed in

Login implementation for Element: https://github.com/vector-im/element-android/blob/afaa89ad42e1803b53c4610169646fedef4c0f68/docs/signin.md#login-with-sso

The sign in activity in the app will likely show a popup to confirm if the user wants to sign in. Maybe an endpoint to introspect the login token can be used to add more details to this popup (name of the user to sign in with for example).

@nielsvanvelzen

To give my view: A flow this way would not be safe. Oauth2 standard requires PKCE for mobile apps, bc otherwise other (malicious) apps could receive the login code.

A safe flow (see also https://datatracker.ietf.org/doc/html/rfc8252 ):

  1. User opens Jellyfin App and wants to do a SSO sign in (the flow must begin at least here!)
  2. App generates a random string called verifier and saves it in a variable. Also it will generate a variable challenge= BASE64URL-ENCODE(SHA256(ASCII(verifier))). verifier will be kept secret until the token exchange.
  3. App will do an API call to jellyfin.myserver.com/login?code_challenge=thechallenge&code_challenge_method=S256 (Alternatively this step can be skipped if the app knows the SSO Provider URL. However the advantage is that the clientid can be kept secret).
  4. Jellyfin will proxy/forward it to sso provider auth.ssoprovider.com/realm/jellyfin/login?client_id=CLIENTID&code_challenge=thechallenge&code_challenge_method=S256&redirect=jellyfin://login (URL depends on the SSO Provider)
  5. The result of the API request will be a redirect (usually as Location: header). This must not be followed
  6. Open up the content of Location: header in a browser an open sign in activity on the SSO provider page
  7. On successful login, the SSO provider will redirect to jellyfin://login?code=CODE with a code.
  8. The code together with verifier from step 2 can now be used to exchange the code for a token.
  9. jellyfin.myserver.com/login/callback?code=CODE&code_verifier=VERIFIER (which can be forwarded to the SSO provider again, and the jellyfin server can retrieve further information like username, email etc.)

The SSO provider checks in a flow beginning with a code_challenge if verifier matches the code, if not the code cannot be used. A malicious app cannot unhash the verifier and as such it is safe.

@j007bond007

This comment was marked as off-topic.

@jellyfin jellyfin locked and limited conversation to collaborators Mar 13, 2024
@thornbill thornbill converted this issue into discussion #68 Mar 13, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
enhancement New feature or request feature-request
Projects
None yet
Development

No branches or pull requests

5 participants