feat: Add support for displaying device code (UserCode) on Android TV SSO flow (#4800)

- Modified URLOpener interface to pass userCode alongside URL in login.go
- added ability to force device auth flow
This commit is contained in:
shuuri-labs
2025-11-25 15:51:16 +01:00
committed by GitHub
parent 20973063d8
commit 7285fef0f0
6 changed files with 21 additions and 16 deletions

View File

@@ -92,7 +92,7 @@ func NewClient(platformFiles PlatformFiles, androidSDKVersion int, deviceName st
} }
// Run start the internal client. It is a blocker function // Run start the internal client. It is a blocker function
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error { func (c *Client) Run(urlOpener URLOpener, isAndroidTV bool, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
exportEnvList(envList) exportEnvList(envList)
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{ cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
ConfigPath: c.cfgFile, ConfigPath: c.cfgFile,
@@ -115,7 +115,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
c.ctxCancelLock.Unlock() c.ctxCancelLock.Unlock()
auth := NewAuthWithConfig(ctx, cfg) auth := NewAuthWithConfig(ctx, cfg)
err = auth.login(urlOpener) err = auth.login(urlOpener, isAndroidTV)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -32,7 +32,7 @@ type ErrListener interface {
// URLOpener it is a callback interface. The Open function will be triggered if // URLOpener it is a callback interface. The Open function will be triggered if
// the backend want to show an url for the user // the backend want to show an url for the user
type URLOpener interface { type URLOpener interface {
Open(string) Open(url string, userCode string)
OnLoginSuccess() OnLoginSuccess()
} }
@@ -148,9 +148,9 @@ func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string
} }
// Login try register the client on the server // Login try register the client on the server
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener) { func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener, isAndroidTV bool) {
go func() { go func() {
err := a.login(urlOpener) err := a.login(urlOpener, isAndroidTV)
if err != nil { if err != nil {
resultListener.OnError(err) resultListener.OnError(err)
} else { } else {
@@ -159,7 +159,7 @@ func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener) {
}() }()
} }
func (a *Auth) login(urlOpener URLOpener) error { func (a *Auth) login(urlOpener URLOpener, isAndroidTV bool) error {
var needsLogin bool var needsLogin bool
// check if we need to generate JWT token // check if we need to generate JWT token
@@ -173,7 +173,7 @@ func (a *Auth) login(urlOpener URLOpener) error {
jwtToken := "" jwtToken := ""
if needsLogin { if needsLogin {
tokenInfo, err := a.foregroundGetTokenInfo(urlOpener) tokenInfo, err := a.foregroundGetTokenInfo(urlOpener, isAndroidTV)
if err != nil { if err != nil {
return fmt.Errorf("interactive sso login failed: %v", err) return fmt.Errorf("interactive sso login failed: %v", err)
} }
@@ -199,8 +199,8 @@ func (a *Auth) login(urlOpener URLOpener) error {
return nil return nil
} }
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) { func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener, isAndroidTV bool) (*auth.TokenInfo, error) {
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false, "") oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false, isAndroidTV, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -210,7 +210,7 @@ func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, err
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err) return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
} }
go urlOpener.Open(flowInfo.VerificationURIComplete) go urlOpener.Open(flowInfo.VerificationURIComplete, flowInfo.UserCode)
waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout) waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout)

View File

@@ -332,7 +332,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
hint = profileState.Email hint = profileState.Email
} }
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop(), hint) oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop(), false, hint)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -60,14 +60,19 @@ func (t TokenInfo) GetTokenToUse() string {
return t.AccessToken return t.AccessToken
} }
func shouldUseDeviceFlow(force bool, isUnixDesktopClient bool) bool {
return force || (runtime.GOOS == "linux" || runtime.GOOS == "freebsd") && !isUnixDesktopClient
}
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration // NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration
// //
// It starts by initializing the PKCE.If this process fails, it resorts to the Device Code Flow, // It starts by initializing the PKCE.If this process fails, it resorts to the Device Code Flow,
// and if that also fails, the authentication process is deemed unsuccessful // and if that also fails, the authentication process is deemed unsuccessful
// //
// On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow // On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow
func NewOAuthFlow(ctx context.Context, config *profilemanager.Config, isUnixDesktopClient bool, hint string) (OAuthFlow, error) { // forceDeviceCodeFlow can be used to skip PKCE and go directly to Device Code Flow (e.g., for Android TV)
if (runtime.GOOS == "linux" || runtime.GOOS == "freebsd") && !isUnixDesktopClient { func NewOAuthFlow(ctx context.Context, config *profilemanager.Config, isUnixDesktopClient bool, forceDeviceCodeFlow bool, hint string) (OAuthFlow, error) {
if shouldUseDeviceFlow(forceDeviceCodeFlow, isUnixDesktopClient) {
return authenticateWithDeviceCodeFlow(ctx, config, hint) return authenticateWithDeviceCodeFlow(ctx, config, hint)
} }

View File

@@ -228,7 +228,7 @@ func (c *Client) LoginForMobile() string {
ConfigPath: c.cfgFile, ConfigPath: c.cfgFile,
}) })
oAuthFlow, err := auth.NewOAuthFlow(ctx, cfg, false, "") oAuthFlow, err := auth.NewOAuthFlow(ctx, cfg, false, false, "")
if err != nil { if err != nil {
return err.Error() return err.Error()
} }

View File

@@ -504,7 +504,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
if msg.Hint != nil { if msg.Hint != nil {
hint = *msg.Hint hint = *msg.Hint
} }
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsUnixDesktopClient, hint) oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsUnixDesktopClient, false, hint)
if err != nil { if err != nil {
state.Set(internal.StatusLoginFailed) state.Set(internal.StatusLoginFailed)
return nil, err return nil, err
@@ -1235,7 +1235,7 @@ func (s *Server) RequestJWTAuth(
} }
isDesktop := isUnixRunningDesktop() isDesktop := isUnixRunningDesktop()
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isDesktop, hint) oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isDesktop, false, hint)
if err != nil { if err != nil {
return nil, gstatus.Errorf(codes.Internal, "failed to create OAuth flow: %v", err) return nil, gstatus.Errorf(codes.Internal, "failed to create OAuth flow: %v", err)
} }