Login initiates the browser-based OAuth login flow with PKCE.
(ctx context.Context, opts LoginOptions)
| 139 | |
| 140 | // Login initiates the browser-based OAuth login flow with PKCE. |
| 141 | func (m *Manager) Login(ctx context.Context, opts LoginOptions) error { |
| 142 | authEndpoint := m.baseURL + "/oauth/authorizations/new" |
| 143 | tokenEndpoint := m.baseURL + "/oauth/tokens" |
| 144 | callbackAddr := "127.0.0.1:8976" |
| 145 | redirectURI := "http://" + callbackAddr + "/callback" |
| 146 | |
| 147 | state := generateState() |
| 148 | codeVerifier := generateCodeVerifier() |
| 149 | codeChallenge := generateCodeChallenge(codeVerifier) |
| 150 | |
| 151 | // Build authorization URL |
| 152 | u, err := url.Parse(authEndpoint) |
| 153 | if err != nil { |
| 154 | return fmt.Errorf("invalid auth endpoint: %w", err) |
| 155 | } |
| 156 | q := u.Query() |
| 157 | q.Set("client_id", oauthClientID) |
| 158 | q.Set("grant_type", "authorization_code") |
| 159 | q.Set("redirect_uri", redirectURI) |
| 160 | q.Set("state", state) |
| 161 | q.Set("code_challenge", codeChallenge) |
| 162 | q.Set("code_challenge_method", "S256") |
| 163 | q.Set("install_id", installID) |
| 164 | u.RawQuery = q.Encode() |
| 165 | authURL := u.String() |
| 166 | |
| 167 | // Start local callback server |
| 168 | code, err := m.waitForCallback(ctx, state, authURL, callbackAddr, opts) |
| 169 | if err != nil { |
| 170 | return err |
| 171 | } |
| 172 | |
| 173 | // Exchange code for tokens |
| 174 | token, err := exchangeCode(ctx, m.httpClient, tokenEndpoint, code, redirectURI, oauthClientID, codeVerifier, installID) |
| 175 | if err != nil { |
| 176 | return fmt.Errorf("token exchange failed: %w", err) |
| 177 | } |
| 178 | |
| 179 | creds := &Credentials{ |
| 180 | AccessToken: token.AccessToken, |
| 181 | RefreshToken: token.RefreshToken, |
| 182 | OAuthType: "oauth", |
| 183 | TokenEndpoint: tokenEndpoint, |
| 184 | } |
| 185 | if !token.ExpiresAt.IsZero() { |
| 186 | creds.ExpiresAt = token.ExpiresAt.Unix() |
| 187 | } |
| 188 | |
| 189 | return m.store.Save(m.baseURL, creds) |
| 190 | } |
| 191 | |
| 192 | // LoginWithToken stores a pre-provided bearer token. |
| 193 | func (m *Manager) LoginWithToken(token string) error { |
no test coverage detected