Sign in through the server's single sign-on, with a code
The sign-in screen asks the server how it can be signed in to (GET /api/auth/config) and offers what it finds: the password form, and "Sign in with <provider>" when the server can do a device login. The TUI shows a link and a short code, the person approves it in any browser, and the next poll hands over the ordinary session, so it works over SSH where no browser can be opened. The terminal never talks to the identity provider. The password form is hidden when the server has turned password login off. `auth: sso` in config.yaml starts the SSO login straight away, but not right after signing out, where that would sign the person straight back in; any other value is refused when the config is read. Polling honours the server's interval, backs off on slow_down, and gives up after repeated failures rather than retrying forever. A server without /api/auth/config answers 404 and is treated as passwords only, so the sign-in screen is the one it had. Needs terdut-server v0.29.0 for SSO.
This commit is contained in:
@@ -79,6 +79,98 @@ func (c *Client) Login(username, password string) (string, error) {
|
||||
return "", fmt.Errorf("server signed us in but sent no %s cookie", SessionCookie)
|
||||
}
|
||||
|
||||
// AuthConfig asks how the server can be signed in to. It is unauthenticated, so
|
||||
// it works before anybody has signed in.
|
||||
func (c *Client) AuthConfig() (AuthConfig, error) {
|
||||
var cfg AuthConfig
|
||||
req, err := http.NewRequest(http.MethodGet, c.baseURL+"/api/auth/config", nil)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
err = c.do(req, &cfg)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// The ways a device login poll can end other than with a session.
|
||||
var (
|
||||
// ErrDevicePending means nobody has approved yet: poll again after the
|
||||
// interval.
|
||||
ErrDevicePending = errors.New("waiting for approval")
|
||||
|
||||
// ErrDeviceSlowDown means the server was polled faster than it asked. It is
|
||||
// not a failure; poll again, a little slower.
|
||||
ErrDeviceSlowDown = errors.New("polling too fast")
|
||||
|
||||
// ErrDeviceExpired means the person took too long, or the server forgot the
|
||||
// login. ErrDeviceDenied means they refused it.
|
||||
ErrDeviceExpired = errors.New("the sign-in expired")
|
||||
ErrDeviceDenied = errors.New("the sign-in was refused")
|
||||
)
|
||||
|
||||
// StartDeviceLogin asks the server to begin a device login.
|
||||
func (c *Client) StartDeviceLogin() (*DeviceLogin, error) {
|
||||
req, err := c.newRequestWithBody(http.MethodPost, "/api/oidc/device", struct{}{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Del("Cookie")
|
||||
var d DeviceLogin
|
||||
if err := c.do(req, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.DeviceCode == "" || d.UserCode == "" || d.VerificationURL == "" {
|
||||
return nil, errors.New("server started a sign-in but sent no code")
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// PollDeviceLogin asks whether the person has approved. On approval it returns
|
||||
// the session token, which the client also keeps; until then it returns one of
|
||||
// the ErrDevice* errors.
|
||||
func (c *Client) PollDeviceLogin(deviceCode string) (string, error) {
|
||||
req, err := c.newRequestWithBody(http.MethodPost, "/api/oidc/device/token",
|
||||
struct {
|
||||
DeviceCode string `json:"device_code"`
|
||||
}{deviceCode})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Del("Cookie")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusAccepted:
|
||||
return "", ErrDevicePending
|
||||
case http.StatusTooManyRequests:
|
||||
return "", ErrDeviceSlowDown
|
||||
case http.StatusGone:
|
||||
var e struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&e)
|
||||
if e.Error == "denied" {
|
||||
return "", ErrDeviceDenied
|
||||
}
|
||||
return "", ErrDeviceExpired
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return "", statusError(resp)
|
||||
}
|
||||
for _, ck := range resp.Cookies() {
|
||||
if ck.Name == SessionCookie && ck.Value != "" {
|
||||
c.session = ck.Value
|
||||
return ck.Value, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("server signed us in but sent no %s cookie", SessionCookie)
|
||||
}
|
||||
|
||||
// Logout ends the session on the server and forgets it here.
|
||||
func (c *Client) Logout() error {
|
||||
req, err := c.newRequest(http.MethodPost, "/api/logout")
|
||||
|
||||
Reference in New Issue
Block a user