Skip to content

feat: add github device flow for authentication #8232

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

Merged
merged 17 commits into from
Jun 29, 2023
Merged
Prev Previous commit
Fix comments
  • Loading branch information
kylecarbs committed Jun 29, 2023
commit d43bd717bb792e4ee938d61b2d90d579e08069e7
47 changes: 28 additions & 19 deletions coderd/gitauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,49 @@ func (api *API) gitAuthByID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

res := codersdk.GitAuth{
Authenticated: false,
Device: config.DeviceAuth != nil,
AppInstallURL: config.AppInstallURL,
Type: config.Type.Pretty(),
Authenticated: false,
Device: config.DeviceAuth != nil,
AppInstallURL: config.AppInstallURL,
Type: config.Type.Pretty(),
AppInstallations: []codersdk.GitAuthAppInstallation{},
}

link, err := api.Database.GetGitAuthLink(ctx, database.GetGitAuthLinkParams{
ProviderID: config.ID,
UserID: apiKey.UserID,
})
if err == nil {
var eg errgroup.Group
eg.Go(func() (err error) {
res.Authenticated, res.User, err = config.ValidateToken(ctx, link.OAuthAccessToken)
return err
})
eg.Go(func() (err error) {
res.AppInstallations, res.AppInstallable, err = config.AppInstallations(ctx, link.OAuthAccessToken)
return err
})
err = eg.Wait()
if err != nil {
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate token.",
Message: "Failed to get git auth link.",
Detail: err.Error(),
})
return
}
}

httpapi.Write(ctx, w, http.StatusOK, res)
return
}
var eg errgroup.Group
eg.Go(func() (err error) {
res.Authenticated, res.User, err = config.ValidateToken(ctx, link.OAuthAccessToken)
return err
})
eg.Go(func() (err error) {
res.AppInstallations, res.AppInstallable, err = config.AppInstallations(ctx, link.OAuthAccessToken)
return err
})
err = eg.Wait()
if err != nil {
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate token.",
Detail: err.Error(),
})
return
}
if res.AppInstallations == nil {
res.AppInstallations = []codersdk.GitAuthAppInstallation{}
}

httpapi.Write(ctx, w, http.StatusOK, res)
}

Expand Down
59 changes: 28 additions & 31 deletions coderd/gitauth/oauth.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package gitauth

import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/url"
"regexp"
"strings"

"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
Expand Down Expand Up @@ -111,7 +109,11 @@ func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.GitAuthDevi
if c.CodeURL == "" {
return nil, xerrors.New("oauth2: device code URL not set")
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.formatDeviceCodeURL(), nil)
codeURL, err := c.formatDeviceCodeURL()
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, codeURL, nil)
if err != nil {
return nil, err
}
Expand All @@ -130,7 +132,7 @@ func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.GitAuthDevi
return nil, err
}
if r.ErrorDescription != "" {
return nil, xerrors.Errorf("%s", r.ErrorDescription)
return nil, xerrors.New(r.ErrorDescription)
}
return &r.GitAuthDevice, nil
}
Expand All @@ -148,7 +150,11 @@ func (c *DeviceAuth) ExchangeDeviceCode(ctx context.Context, deviceCode string)
if c.TokenURL == "" {
return nil, xerrors.New("oauth2: token URL not set")
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.formatDeviceTokenURL(deviceCode), nil)
tokenURL, err := c.formatDeviceTokenURL(deviceCode)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
if err != nil {
return nil, err
}
Expand All @@ -167,41 +173,32 @@ func (c *DeviceAuth) ExchangeDeviceCode(ctx context.Context, deviceCode string)
return nil, err
}
if body.Error != "" {
return nil, xerrors.Errorf("%s", body.Error)
return nil, xerrors.New(body.Error)
}
return body.Token, nil
}

func (c *DeviceAuth) formatDeviceTokenURL(deviceCode string) string {
var buf bytes.Buffer
_, _ = buf.WriteString(c.TokenURL)
v := url.Values{
func (c *DeviceAuth) formatDeviceTokenURL(deviceCode string) (string, error) {
tok, err := url.Parse(c.TokenURL)
if err != nil {
return "", err
}
tok.RawQuery = url.Values{
"client_id": {c.ClientID},
"device_code": {deviceCode},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
}
if strings.Contains(c.TokenURL, "?") {
_ = buf.WriteByte('&')
} else {
_ = buf.WriteByte('?')
}
_, _ = buf.WriteString(v.Encode())
return buf.String()
}.Encode()
return tok.String(), nil
}

func (c *DeviceAuth) formatDeviceCodeURL() string {
var buf bytes.Buffer
_, _ = buf.WriteString(c.CodeURL)

v := url.Values{
func (c *DeviceAuth) formatDeviceCodeURL() (string, error) {
cod, err := url.Parse(c.CodeURL)
if err != nil {
return "", err
}
cod.RawQuery = url.Values{
"client_id": {c.ClientID},
"scope": c.Scopes,
}
if strings.Contains(c.CodeURL, "?") {
_ = buf.WriteByte('&')
} else {
_ = buf.WriteByte('?')
}
_, _ = buf.WriteString(v.Encode())
return buf.String()
}.Encode()
return cod.String(), nil
}