providers: Kiro is a subscription added in magpie, signed in on Kiro's own page (Image #21: 为什么在订阅列表里看不到Kiro订阅) — Kiro was used through kiro-cli's or the Kiro IDE's sign-in, or a key, so it had no card among the subscriptions and no way to sign in from magpie. It now has a card, and signing in opens app.kiro.dev as the IDE does: Google and GitHub are exchanged with Kiro's auth service, Builder ID and Identity Center go on to AWS with a client registered for magpie, and the sign-in is kept in magpie's own kiro-auth-token.json, refreshed there, and used ahead of kiro-cli's and the IDE's, which are left as they were. A company's own identity provider is still kiro-cli's (kiro-cli login), and the sign-in says so. Tested with a fake Kiro and AWS (Google, Builder ID, a stale answer, an external IdP) and against the live page, token and register endpoints; not yet with a real Kiro account
This commit is contained in:
6 files changed
+498
-4
No files matched your search
@@ -3643,6 +3643,8 @@ const SUBS = [
|
||||
{ agent: "commandcode-plan", name: "Command Code", icon: "commandcode", plans: "Pro · GOAT · Max · Ultra", own: true },
|
||||
// devin's credentials.toml keeps one account too
|
||||
{ agent: "devin", name: "Devin", icon: "devin", plans: "Pro · Enterprise", single: true },
|
||||
// Kiro's own sign-in page (Google, GitHub, Builder ID, Identity Center); kiro-cli's or the IDE's is read too
|
||||
{ agent: "kiro", name: "Kiro", icon: "kiro-color", plans: "Free · Pro · Pro+ · Power", single: true },
|
||||
// Google's sign-ins; Gemini CLI's own account is read too
|
||||
{ agent: "gemini", name: "Gemini CLI", icon: "geminicli-color", plans: "Code Assist Standard · Enterprise", own: true },
|
||||
// accounts can also come from another tool's export (Antigravity Cockpit, Antigravity Manager, CLIProxyAPI)
|
||||
|
||||
@@ -212,7 +212,7 @@ func providerInfo(p provider.Provider, agents []agentUse) providerJSON {
|
||||
// a Cursor subscription is served by the gateway, not an agent magpie configures
|
||||
out.Account.Name, out.Account.Icon = "Cursor CLI", "cursor"
|
||||
} else if a.Agent == "kiro" {
|
||||
// Kiro's sign-in is kiro-cli's or the Kiro IDE's
|
||||
// Kiro's sign-in is magpie's own, kiro-cli's or the Kiro IDE's
|
||||
out.Account.Name, out.Account.Icon = "Kiro", "kiro-color"
|
||||
} else if a.Agent == "antigravity" {
|
||||
out.Account.Name, out.Account.Icon = "Antigravity", "antigravity-color"
|
||||
|
||||
@@ -200,12 +200,15 @@ func readKiroIDE() (kiroCred, bool) {
|
||||
return c, true
|
||||
}
|
||||
|
||||
// readKiro is the sign-in to use: the key saved on the provider, else
|
||||
// kiro-cli's, else the IDE's.
|
||||
// readKiro is the sign-in to use: the key saved on the provider, else the
|
||||
// one made in magpie, else kiro-cli's, else the IDE's.
|
||||
func readKiro(key string) (kiroCred, bool) {
|
||||
if key != "" {
|
||||
return kiroCred{access: key, method: "apikey", region: "us-east-1"}, true
|
||||
}
|
||||
if c, ok := readKiroMagpie(); ok {
|
||||
return c, true
|
||||
}
|
||||
if c, ok := readKiroCLI(); ok {
|
||||
return c, true
|
||||
}
|
||||
@@ -253,7 +256,7 @@ func KiroAuthOf(ctx context.Context, key string, stale bool) (KiroAuth, error) {
|
||||
read, ok := readKiro(key)
|
||||
if !ok {
|
||||
kiroAuthCache.ok = false
|
||||
return KiroAuth{}, errors.New("Kiro isn't signed in; sign in with `kiro-cli login` or the Kiro IDE, or save a Kiro API key on the provider")
|
||||
return KiroAuth{}, errors.New("Kiro isn't signed in; add the Kiro subscription in magpie, sign in with `kiro-cli login` or the Kiro IDE, or save a Kiro API key on the provider")
|
||||
}
|
||||
c := kiroAuthCache.cred
|
||||
if !kiroAuthCache.ok || kiroAuthCache.key != key || read.access != c.access || !c.fresh() || stale {
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package provider
|
||||
|
||||
// Signing in to Kiro from magpie, the way the Kiro IDE does: Kiro's sign-in
|
||||
// page (app.kiro.dev) offers Google, GitHub, AWS Builder ID and IAM
|
||||
// Identity Center, and sends the browser back to a port on this machine.
|
||||
// Google and GitHub come back with a code Kiro's auth service trades for
|
||||
// tokens; Builder ID and Identity Center come back with where to sign in
|
||||
// at AWS, which is then done with a client registered for it, as the IDE
|
||||
// does. The sign-in is magpie's own, kept beside logins.json, so kiro-cli's
|
||||
// and the IDE's are left as they are; while it is there it is the one used.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Where Kiro signs in; vars so tests can answer them.
|
||||
var (
|
||||
kiroPortalURL = "https://app.kiro.dev"
|
||||
kiroAuthService = "https://prod.us-east-1.auth.desktop.kiro.dev"
|
||||
kiroOIDC = func(region string) string { return "https://oidc." + region + ".amazonaws.com" }
|
||||
// the ports Kiro's sign-in page sends the browser back to, the IDE's
|
||||
kiroCallbackPorts = []int{3128, 4649, 6588, 8008, 9091, 49153, 50153, 51153, 52153, 53153}
|
||||
)
|
||||
|
||||
// kiroScopes are what the IDE asks AWS for.
|
||||
var kiroScopes = []string{"codewhisperer:completions", "codewhisperer:analysis", "codewhisperer:conversations",
|
||||
"codewhisperer:transformations", "codewhisperer:taskassist"}
|
||||
|
||||
// kiroIDEUA is how the IDE names itself to Kiro's auth service.
|
||||
var kiroIDEUA = func() string {
|
||||
h, _ := os.Hostname()
|
||||
sum := sha256.Sum256([]byte("magpie:" + h))
|
||||
return "KiroIDE-1.1.70-" + hex.EncodeToString(sum[:])
|
||||
}()
|
||||
|
||||
// kiroSignInPath is magpie's own Kiro sign-in.
|
||||
func kiroSignInPath() string { return filepath.Join(filepath.Dir(Path()), "kiro-auth-token.json") }
|
||||
|
||||
// kiroSaved is magpie's Kiro sign-in as it is kept: the IDE's shape, with
|
||||
// an AWS sign-in's client beside it.
|
||||
type kiroSaved struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
AuthMethod string `json:"authMethod"` // social | IdC
|
||||
Provider string `json:"provider"` // Google, Github, BuilderId, Enterprise
|
||||
Region string `json:"region,omitempty"`
|
||||
ProfileArn string `json:"profileArn,omitempty"`
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
ClientSecret string `json:"clientSecret,omitempty"`
|
||||
}
|
||||
|
||||
// readKiroMagpie reads the sign-in made in magpie.
|
||||
func readKiroMagpie() (kiroCred, bool) {
|
||||
path := kiroSignInPath()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return kiroCred{}, false
|
||||
}
|
||||
var t kiroSaved
|
||||
if json.Unmarshal(b, &t) != nil || t.AccessToken == "" {
|
||||
return kiroCred{}, false
|
||||
}
|
||||
c := kiroCred{access: t.AccessToken, refresh: t.RefreshToken, region: t.Region, profile: t.ProfileArn,
|
||||
clientID: t.ClientID, clientSecret: t.ClientSecret, idePath: path}
|
||||
c.expires, _ = time.Parse(time.RFC3339Nano, t.ExpiresAt)
|
||||
if c.region == "" {
|
||||
c.region = "us-east-1"
|
||||
}
|
||||
if strings.EqualFold(t.AuthMethod, "IdC") && c.clientID != "" {
|
||||
c.method = "idc"
|
||||
} else {
|
||||
c.method, c.social = "social", true
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
|
||||
// kiroFlow is where a Kiro sign-in is between its two pages.
|
||||
type kiroFlow struct {
|
||||
port int
|
||||
// an AWS sign-in, once Kiro's page has sent the browser on to it
|
||||
region, clientID, clientSecret, provider string
|
||||
state, verifier string
|
||||
}
|
||||
|
||||
// startKiroSignIn opens Kiro's sign-in page, to come back to one of the
|
||||
// ports it sends browsers back to.
|
||||
func startKiroSignIn(s *signInFlow, challenge string) (net.Listener, error) {
|
||||
var ln net.Listener
|
||||
var err error
|
||||
for _, p := range kiroCallbackPorts {
|
||||
if ln, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", p)); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ln == nil {
|
||||
return nil, errors.New("the ports Kiro's sign-in comes back to are all busy; close the Kiro IDE's sign-in and try again")
|
||||
}
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
s.kiro = &kiroFlow{port: port}
|
||||
s.redirect = fmt.Sprintf("http://localhost:%d", port)
|
||||
q := url.Values{}
|
||||
q.Set("state", s.state)
|
||||
q.Set("code_challenge", challenge)
|
||||
q.Set("code_challenge_method", "S256")
|
||||
q.Set("redirect_uri", s.redirect)
|
||||
q.Set("redirect_from", "KiroIDE")
|
||||
s.mu.Lock()
|
||||
s.st.URL = kiroPortalURL + "/signin?" + q.Encode()
|
||||
s.mu.Unlock()
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
// kiroCallback is the browser back from Kiro's page, or from AWS's.
|
||||
func (s *signInFlow) kiroCallback(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/oauth/callback", "/signin/callback":
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
if s.status().State != "waiting" {
|
||||
signInPage(w, false, "This sign-in is over", "Start it again from magpie.")
|
||||
return
|
||||
}
|
||||
fail := func(msg string) {
|
||||
s.finish(SignInState{State: "failed", Error: msg})
|
||||
signInPage(w, false, "Sign-in didn't finish", msg)
|
||||
}
|
||||
if e := q.Get("error"); e != "" {
|
||||
msg := q.Get("error_description")
|
||||
if msg == "" {
|
||||
msg = e
|
||||
}
|
||||
fail(msg)
|
||||
return
|
||||
}
|
||||
k := s.kiro
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
var saved kiroSaved
|
||||
switch {
|
||||
case k.state != "" && q.Get("state") == k.state:
|
||||
// back from AWS
|
||||
if q.Get("code") == "" {
|
||||
fail("AWS sent back no code")
|
||||
return
|
||||
}
|
||||
var tok struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ExpiresIn int `json:"expiresIn"`
|
||||
}
|
||||
err := kiroSignInPost(ctx, kiroOIDC(k.region)+"/token", map[string]string{"clientId": k.clientID, "clientSecret": k.clientSecret,
|
||||
"grantType": "authorization_code", "redirectUri": k.awsRedirect(), "code": q.Get("code"), "codeVerifier": k.verifier}, &tok)
|
||||
if err == nil && tok.AccessToken == "" {
|
||||
err = errors.New("AWS sent back no token")
|
||||
}
|
||||
if err != nil {
|
||||
fail(err.Error())
|
||||
return
|
||||
}
|
||||
saved = kiroSaved{AccessToken: tok.AccessToken, RefreshToken: tok.RefreshToken, ExpiresAt: kiroExpiry(tok.ExpiresIn),
|
||||
AuthMethod: "IdC", Provider: k.provider, Region: k.region, ClientID: k.clientID, ClientSecret: k.clientSecret}
|
||||
case q.Get("state") != s.state:
|
||||
// not ours: someone else's page, or a stale tab
|
||||
signInPage(w, false, "This link isn't from magpie's sign-in", "Start it again from magpie.")
|
||||
return
|
||||
default:
|
||||
switch opt := q.Get("login_option"); opt {
|
||||
case "google", "github":
|
||||
var tok struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ProfileArn string `json:"profileArn"`
|
||||
ExpiresIn int `json:"expiresIn"`
|
||||
}
|
||||
err := kiroSignInPost(ctx, kiroAuthService+"/oauth/token", map[string]string{"code": q.Get("code"),
|
||||
"code_verifier": s.verifier, "redirect_uri": s.redirect + r.URL.Path + "?login_option=" + opt}, &tok)
|
||||
if err == nil && tok.AccessToken == "" {
|
||||
err = errors.New("Kiro sent back no token")
|
||||
}
|
||||
if err != nil {
|
||||
fail(err.Error())
|
||||
return
|
||||
}
|
||||
provider := map[string]string{"google": "Google", "github": "Github"}[opt]
|
||||
saved = kiroSaved{AccessToken: tok.AccessToken, RefreshToken: tok.RefreshToken, ExpiresAt: kiroExpiry(tok.ExpiresIn),
|
||||
AuthMethod: "social", Provider: provider, Region: "us-east-1", ProfileArn: tok.ProfileArn}
|
||||
case "builderid", "awsidc", "internal":
|
||||
// on to AWS, with a client registered for this sign-in
|
||||
issuer, region := q.Get("issuer_url"), q.Get("idc_region")
|
||||
if issuer == "" || region == "" {
|
||||
fail("Kiro's page didn't say where to sign in at AWS")
|
||||
return
|
||||
}
|
||||
var reg struct {
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
err := kiroSignInPost(ctx, kiroOIDC(region)+"/client/register", map[string]any{"clientName": "Kiro IDE", "clientType": "public",
|
||||
"scopes": kiroScopes, "grantTypes": []string{"authorization_code", "refresh_token"},
|
||||
"redirectUris": []string{"http://127.0.0.1/oauth/callback"}, "issuerUrl": issuer}, ®)
|
||||
if err == nil && reg.ClientID == "" {
|
||||
err = errors.New("AWS registered no client")
|
||||
}
|
||||
if err != nil {
|
||||
fail(err.Error())
|
||||
return
|
||||
}
|
||||
k.region, k.clientID, k.clientSecret = region, reg.ClientID, reg.ClientSecret
|
||||
k.provider = map[string]string{"builderid": "BuilderId", "awsidc": "Enterprise", "internal": "Internal"}[opt]
|
||||
k.state, k.verifier = randomToken(24), randomToken(48)
|
||||
sum := sha256.Sum256([]byte(k.verifier))
|
||||
a := url.Values{}
|
||||
a.Set("response_type", "code")
|
||||
a.Set("client_id", k.clientID)
|
||||
a.Set("redirect_uri", k.awsRedirect())
|
||||
a.Set("scopes", strings.Join(kiroScopes, ","))
|
||||
a.Set("state", k.state)
|
||||
a.Set("code_challenge", base64.RawURLEncoding.EncodeToString(sum[:]))
|
||||
a.Set("code_challenge_method", "S256")
|
||||
http.Redirect(w, r, kiroOIDC(region)+"/authorize?"+a.Encode(), http.StatusFound)
|
||||
return
|
||||
case "external_idp":
|
||||
fail("magpie can't sign in with a company's own identity provider yet; sign in with `kiro-cli login`, and magpie uses that")
|
||||
return
|
||||
default:
|
||||
fail(fmt.Sprintf("Kiro's page came back with a sign-in magpie doesn't know (%q)", opt))
|
||||
return
|
||||
}
|
||||
}
|
||||
b, _ := json.MarshalIndent(saved, "", " ")
|
||||
if err := writePrivate(kiroSignInPath(), append(b, '\n')); err != nil {
|
||||
fail(err.Error())
|
||||
return
|
||||
}
|
||||
forgetAccountCaches()
|
||||
user, plan := askKiroIdentity("")
|
||||
kiroStatus.Lock()
|
||||
kiroStatus.key, kiroStatus.user, kiroStatus.plan, kiroStatus.at = "", user, plan, time.Now()
|
||||
kiroStatus.Unlock()
|
||||
if user == "" {
|
||||
user = "Kiro account"
|
||||
}
|
||||
s.finish(SignInState{State: "done", User: user, Plan: plan, Using: true})
|
||||
signInPage(w, true, "You're signed in", fmt.Sprintf("%s is added to magpie. You can close this tab.", user))
|
||||
}
|
||||
|
||||
// awsRedirect is where AWS sends the browser back.
|
||||
func (k *kiroFlow) awsRedirect() string {
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/oauth/callback", k.port)
|
||||
}
|
||||
|
||||
func kiroExpiry(in int) string {
|
||||
if in <= 0 {
|
||||
in = 3600
|
||||
}
|
||||
return time.Now().Add(time.Duration(in) * time.Second).UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
// kiroSignInPost posts JSON to Kiro's auth service or AWS's sign-in.
|
||||
func kiroSignInPost(ctx context.Context, u string, body any, dst any) error {
|
||||
b, _ := json.Marshal(body)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", kiroIDEUA)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
out, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if res.StatusCode/100 != 2 {
|
||||
var e struct {
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
_ = json.Unmarshal(out, &e)
|
||||
msg := e.ErrorDescription
|
||||
if msg == "" {
|
||||
msg = e.Message
|
||||
}
|
||||
if msg == "" {
|
||||
msg = e.Error
|
||||
}
|
||||
if msg == "" {
|
||||
msg = http.StatusText(res.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("the sign-in was refused (%d): %s", res.StatusCode, msg)
|
||||
}
|
||||
return json.Unmarshal(out, dst)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// kiroFakeSignIn answers Kiro's auth service and AWS's sign-in, checking
|
||||
// what the IDE would send; it returns the sign-in's page and a browser that
|
||||
// doesn't follow redirects.
|
||||
func kiroFakeSignIn(t *testing.T) (start func() (SignInState, url.Values), browser *http.Client) {
|
||||
t.Helper()
|
||||
kiroSandbox(t)
|
||||
var challenge, redirect string
|
||||
var awsChallenge string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var in map[string]any
|
||||
json.NewDecoder(r.Body).Decode(&in)
|
||||
str := func(k string) string { s, _ := in[k].(string); return s }
|
||||
verify := func(verifier, want string) bool {
|
||||
sum := sha256.Sum256([]byte(verifier))
|
||||
return base64.RawURLEncoding.EncodeToString(sum[:]) == want
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/oauth/token": // Kiro's, for Google and GitHub
|
||||
if str("code") != "gcode" || !verify(str("code_verifier"), challenge) || str("redirect_uri") != redirect+"/oauth/callback?login_option=google" ||
|
||||
!strings.HasPrefix(r.Header.Get("User-Agent"), "KiroIDE-") {
|
||||
t.Errorf("Kiro token request %v", in)
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"accessToken": "social-at", "refreshToken": "social-rt", "profileArn": "arn:aws:codewhisperer:us-east-1:1:profile/P", "expiresIn": 3600})
|
||||
case "/client/register":
|
||||
if in["issuerUrl"] != "https://view.awsapps.com/start" || in["clientType"] != "public" {
|
||||
t.Errorf("register %v", in)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"clientId": "cid", "clientSecret": "csecret", "clientSecretExpiresAt": time.Now().Add(90 * 24 * time.Hour).Unix()})
|
||||
case "/token": // AWS's
|
||||
if str("grantType") != "authorization_code" || str("code") != "awscode" || str("clientId") != "cid" || str("clientSecret") != "csecret" ||
|
||||
!verify(str("codeVerifier"), awsChallenge) || !strings.HasPrefix(str("redirectUri"), "http://127.0.0.1:") {
|
||||
t.Errorf("AWS token request %v", in)
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"accessToken": "idc-at", "refreshToken": "idc-rt", "expiresIn": 3600})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
auth, oidc, ports, ask := kiroAuthService, kiroOIDC, kiroCallbackPorts, askKiroIdentity
|
||||
kiroAuthService, kiroOIDC, kiroCallbackPorts = srv.URL, func(string) string { return srv.URL }, []int{0}
|
||||
askKiroIdentity = func(string) (string, string) {
|
||||
if c, ok := readKiro(""); ok && c.access == "social-at" {
|
||||
return "me@example.com", "KIRO PRO"
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
t.Cleanup(func() { kiroAuthService, kiroOIDC, kiroCallbackPorts, askKiroIdentity = auth, oidc, ports, ask })
|
||||
browser = &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
||||
start = func() (SignInState, url.Values) {
|
||||
st, err := StartSignIn("kiro")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { CancelSignIn(st.ID) })
|
||||
u, _ := url.Parse(st.URL)
|
||||
q := u.Query()
|
||||
if !strings.HasPrefix(st.URL, "https://app.kiro.dev/signin?") || q.Get("redirect_from") != "KiroIDE" || q.Get("code_challenge_method") != "S256" {
|
||||
t.Fatalf("page %s", st.URL)
|
||||
}
|
||||
challenge, redirect = q.Get("code_challenge"), q.Get("redirect_uri")
|
||||
return st, q
|
||||
}
|
||||
kiroSetAWSChallenge = func(c string) { awsChallenge = c }
|
||||
return start, browser
|
||||
}
|
||||
|
||||
// kiroSetAWSChallenge tells the fake AWS the challenge its page was sent.
|
||||
var kiroSetAWSChallenge func(string)
|
||||
|
||||
// Signed in with Google on Kiro's page, the account is magpie's own and
|
||||
// the one used, ahead of kiro-cli's, which is left as it was.
|
||||
func TestKiroSignInWithGoogle(t *testing.T) {
|
||||
start, browser := kiroFakeSignIn(t)
|
||||
writeKiroCLI(t, map[string]any{"kirocli:social:token": map[string]any{"access_token": "cli-at", "expires_at": "2099-01-01T00:00:00Z"}})
|
||||
st, q := start()
|
||||
res, err := browser.Get(q.Get("redirect_uri") + "/oauth/callback?login_option=google&code=gcode&state=" + url.QueryEscape(q.Get("state")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
if st, _ = SignInStatus(st.ID); st.State != "done" || st.User != "me@example.com" || st.Plan != "KIRO PRO" || !st.Using {
|
||||
t.Fatalf("status %+v", st)
|
||||
}
|
||||
c, ok := readKiro("")
|
||||
if !ok || c.access != "social-at" || c.refresh != "social-rt" || c.method != "social" || c.profile != "arn:aws:codewhisperer:us-east-1:1:profile/P" || !c.fresh() {
|
||||
t.Fatalf("cred %+v", c)
|
||||
}
|
||||
if row := readKiroRow(t, "kirocli:social:token"); row["access_token"] != "cli-at" {
|
||||
t.Fatalf("kiro-cli's sign-in changed: %v", row)
|
||||
}
|
||||
if p, ok := kiroAccount(); !ok || p.Account.User != "me@example.com" {
|
||||
t.Fatalf("account %+v", p.Account)
|
||||
}
|
||||
}
|
||||
|
||||
// Builder ID goes on from Kiro's page to AWS's, with a client registered
|
||||
// for it, and comes back an AWS sign-in magpie can refresh.
|
||||
func TestKiroSignInWithBuilderID(t *testing.T) {
|
||||
start, browser := kiroFakeSignIn(t)
|
||||
st, q := start()
|
||||
res, err := browser.Get(q.Get("redirect_uri") + "/signin/callback?login_option=builderid&issuer_url=" + url.QueryEscape("https://view.awsapps.com/start") +
|
||||
"&idc_region=us-east-1&state=" + url.QueryEscape(q.Get("state")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
loc, _ := url.Parse(res.Header.Get("Location"))
|
||||
a := loc.Query()
|
||||
if res.StatusCode != http.StatusFound || loc.Path != "/authorize" || a.Get("client_id") != "cid" || a.Get("response_type") != "code" ||
|
||||
!strings.Contains(a.Get("scopes"), "codewhisperer:conversations") || !strings.HasSuffix(a.Get("redirect_uri"), "/oauth/callback") {
|
||||
t.Fatalf("on to AWS: %d %s", res.StatusCode, loc)
|
||||
}
|
||||
kiroSetAWSChallenge(a.Get("code_challenge"))
|
||||
// a stale tab's answer is turned away
|
||||
if res, err = browser.Get(a.Get("redirect_uri") + "?code=awscode&state=nope"); err == nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
if st, _ = SignInStatus(st.ID); st.State != "waiting" {
|
||||
t.Fatalf("a stranger's answer ended it: %+v", st)
|
||||
}
|
||||
if res, err = browser.Get(a.Get("redirect_uri") + "?code=awscode&state=" + url.QueryEscape(a.Get("state"))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
if st, _ = SignInStatus(st.ID); st.State != "done" || st.User != "Kiro account" {
|
||||
t.Fatalf("status %+v", st)
|
||||
}
|
||||
c, ok := readKiro("")
|
||||
if !ok || c.access != "idc-at" || c.method != "idc" || c.clientID != "cid" || c.clientSecret != "csecret" || c.region != "us-east-1" {
|
||||
t.Fatalf("cred %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
// A company's own identity provider is left to kiro-cli, and said so.
|
||||
func TestKiroSignInExternalIdP(t *testing.T) {
|
||||
start, browser := kiroFakeSignIn(t)
|
||||
st, q := start()
|
||||
res, err := browser.Get(q.Get("redirect_uri") + "/oauth/callback?login_option=external_idp&state=" + url.QueryEscape(q.Get("state")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
if st, _ = SignInStatus(st.ID); st.State != "failed" || !strings.Contains(st.Error, "kiro-cli login") {
|
||||
t.Fatalf("status %+v", st)
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ type signInFlow struct {
|
||||
redirect string
|
||||
srv *http.Server
|
||||
stop func() // ends an agent's own login command, when that is the sign-in
|
||||
kiro *kiroFlow
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
@@ -228,6 +229,11 @@ func (s *signInFlow) begin() error {
|
||||
s.mu.Lock()
|
||||
s.st.URL = devinAuthorizeURL + "?" + q.Encode()
|
||||
s.mu.Unlock()
|
||||
case "kiro":
|
||||
// Kiro's sign-in page, as the Kiro IDE opens it
|
||||
if ln, err = startKiroSignIn(s, challenge); err != nil {
|
||||
return err
|
||||
}
|
||||
case "copilot":
|
||||
// GitHub's device code, as Copilot's editors sign in
|
||||
if err := startCopilotSignIn(s); err != nil {
|
||||
@@ -367,6 +373,10 @@ func (s *signInFlow) finish(out SignInState) bool {
|
||||
}
|
||||
|
||||
func (s *signInFlow) callback(w http.ResponseWriter, r *http.Request) {
|
||||
if s.kiro != nil {
|
||||
s.kiroCallback(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/callback", "/auth/callback", "/oauth2callback", "/oauth-callback":
|
||||
case "/cancel":
|
||||
|
||||
Reference in new issue
Block a user