Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitcbd75f5

Browse files
committed
Add OAuth2 auth, token, and revoke routes
1 parent9b74eb3 commitcbd75f5

File tree

7 files changed

+953
-1
lines changed

7 files changed

+953
-1
lines changed

‎codersdk/oauth2.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,22 @@ func (c *Client) DeleteOAuth2ProviderAppSecret(ctx context.Context, appID uuid.U
167167
}
168168
returnnil
169169
}
170+
171+
typeOAuth2ProviderGrantTypestring
172+
173+
const (
174+
OAuth2ProviderGrantTypeAuthorizationCodeOAuth2ProviderGrantType="authorization_code"
175+
)
176+
177+
// RevokeOAuth2ProviderApp completely revokes an app's access for the user.
178+
func (c*Client)RevokeOAuth2ProviderApp(ctx context.Context,appID uuid.UUID)error {
179+
res,err:=c.Request(ctx,http.MethodDelete,fmt.Sprintf("/api/v2/oauth2-provider/apps/%s/tokens",appID),nil)
180+
iferr!=nil {
181+
returnerr
182+
}
183+
deferres.Body.Close()
184+
ifres.StatusCode!=http.StatusNoContent {
185+
returnReadBodyAsError(res)
186+
}
187+
returnnil
188+
}

‎enterprise/coderd/coderd.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
164164
returnnil,xerrors.Errorf("failed to get deployment ID: %w",err)
165165
}
166166

167+
api.AGPL.RootHandler.Group(func(r chi.Router) {
168+
r.Use(
169+
api.oAuth2ProviderMiddleware,
170+
apiKeyMiddlewareOptional,
171+
httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderApp(options.Database)),
172+
)
173+
// Oauth2 linking routes do not make sense under the /api/v2 path.
174+
r.Route("/login",func(r chi.Router) {
175+
r.Route("/oauth2",func(r chi.Router) {
176+
r.Get("/authorize",api.postOAuth2ProviderAppAuthorize())
177+
r.Post("/tokens",api.postOAuth2ProviderAppToken())
178+
})
179+
})
180+
})
181+
167182
api.AGPL.APIHandler.Group(func(r chi.Router) {
168183
r.Get("/entitlements",api.serveEntitlements)
169184
// /regions overrides the AGPL /regions endpoint
@@ -334,6 +349,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
334349
r.Get("/",api.oAuth2ProviderApp)
335350
r.Put("/",api.putOAuth2ProviderApp)
336351
r.Delete("/",api.deleteOAuth2ProviderApp)
352+
r.Delete("/tokens",api.deleteOAuth2ProviderAppTokens)
337353

338354
r.Route("/secrets",func(r chi.Router) {
339355
r.Get("/",api.oAuth2ProviderAppSecrets)
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package identityprovider
2+
3+
import (
4+
"crypto/sha256"
5+
"net/http"
6+
"net/url"
7+
"strings"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"golang.org/x/xerrors"
12+
13+
"github.com/coder/coder/v2/coderd/database"
14+
"github.com/coder/coder/v2/coderd/database/dbtime"
15+
"github.com/coder/coder/v2/coderd/httpapi"
16+
"github.com/coder/coder/v2/coderd/httpmw"
17+
"github.com/coder/coder/v2/codersdk"
18+
"github.com/coder/coder/v2/cryptorand"
19+
)
20+
21+
/**
22+
* Authorize displays an HTML for authorizing an application when the user has
23+
* first been redirected to this path and generates a code and redirects to the
24+
* app's callback URL after the user clicks "allow" on that page.
25+
*/
26+
funcAuthorize(db database.Store,accessURL*url.URL) http.HandlerFunc {
27+
handler:=func(rw http.ResponseWriter,r*http.Request) {
28+
ctx:=r.Context()
29+
apiKey,ok:=httpmw.APIKeyOptional(r)
30+
if!ok {
31+
// TODO: Should this be unauthorized? Or Forbidden?
32+
// This should redirect to a login page.
33+
httpapi.Forbidden(rw)
34+
return
35+
}
36+
37+
app:=httpmw.OAuth2ProviderApp(r)
38+
39+
// TODO: @emyrk this should always work, maybe make callbackURL a *url.URL?
40+
callbackURL,_:=url.Parse(app.CallbackURL)
41+
42+
// TODO: Should validate these on the HTML page as well and show errors
43+
// there rather than wait until this endpoint to show them.
44+
p:=httpapi.NewQueryParamParser()
45+
vals:=r.URL.Query()
46+
p.Required("state","response_type")
47+
state:=p.String(vals,"","state")
48+
scope:=p.Strings(vals, []string{},"scope")
49+
// Client_id is already parsed in the mw above.
50+
_=p.String(vals,"","client_id")
51+
redirectURL:=p.URL(vals,callbackURL,"redirect_uri")
52+
responseType:=p.String(vals,"","response_type")
53+
// TODO: Redirected might exist but it should not cause validation errors.
54+
_=p.String(vals,"","redirected")
55+
p.ErrorExcessParams(vals)
56+
iflen(p.Errors)>0 {
57+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
58+
Message:"Invalid query params.",
59+
Validations:p.Errors,
60+
})
61+
return
62+
}
63+
64+
// TODO: @emyrk what other ones are there?
65+
ifresponseType!="code" {
66+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
67+
Message:"Invalid response type.",
68+
})
69+
return
70+
}
71+
72+
// TODO: @emyrk handle scope?
73+
_=scope
74+
75+
iferr:=validateRedirectURL(app.CallbackURL,redirectURL.String());err!=nil {
76+
httpapi.Write(r.Context(),rw,http.StatusBadRequest, codersdk.Response{
77+
Message:"Invalid redirect URL.",
78+
})
79+
return
80+
}
81+
// 40 characters matches the length of GitHub's client secrets.
82+
rawSecret,err:=cryptorand.String(40)
83+
iferr!=nil {
84+
httpapi.Write(r.Context(),rw,http.StatusInternalServerError, codersdk.Response{
85+
Message:"Failed to generate OAuth2 app authorization code.",
86+
})
87+
return
88+
}
89+
hashed:=sha256.Sum256([]byte(rawSecret))
90+
_,err=db.InsertOAuth2ProviderAppCode(ctx, database.InsertOAuth2ProviderAppCodeParams{
91+
ID:uuid.New(),
92+
CreatedAt:dbtime.Now(),
93+
// TODO: Configurable expiration?
94+
ExpiresAt:dbtime.Now().Add(time.Duration(10)*time.Minute),
95+
HashedSecret:hashed[:],
96+
AppID:app.ID,
97+
UserID:apiKey.UserID,
98+
})
99+
iferr!=nil {
100+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
101+
Message:"Internal error insert OAuth2 authorization code.",
102+
Detail:err.Error(),
103+
})
104+
return
105+
}
106+
107+
newQuery:=redirectURL.Query()
108+
newQuery.Add("code",rawSecret)
109+
newQuery.Add("state",state)
110+
redirectURL.RawQuery=newQuery.Encode()
111+
112+
http.Redirect(rw,r,redirectURL.String(),http.StatusTemporaryRedirect)
113+
}
114+
115+
// Always wrap with its custom mw.
116+
returnauthorizeMW(accessURL)(http.HandlerFunc(handler)).ServeHTTP
117+
}
118+
119+
// validateRedirectURL validates that the redirectURL is contained in baseURL.
120+
funcvalidateRedirectURL(baseURLstring,redirectURLstring)error {
121+
base,err:=url.Parse(baseURL)
122+
iferr!=nil {
123+
returnerr
124+
}
125+
126+
redirect,err:=url.Parse(redirectURL)
127+
iferr!=nil {
128+
returnerr
129+
}
130+
// It can be a sub-directory but not a sub-domain, as we have apps on
131+
// sub-domains so it seems too dangerous.
132+
ifredirect.Host!=base.Host||!strings.HasPrefix(redirect.Path,base.Path) {
133+
returnxerrors.New("invalid redirect URL")
134+
}
135+
returnnil
136+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package identityprovider
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
7+
"github.com/coder/coder/v2/coderd/httpapi"
8+
"github.com/coder/coder/v2/coderd/httpmw"
9+
"github.com/coder/coder/v2/codersdk"
10+
"github.com/coder/coder/v2/site"
11+
)
12+
13+
// authorizeMW serves to remove some code from the primary authorize handler.
14+
// It decides when to show the html allow page, and when to just continue.
15+
funcauthorizeMW(accessURL*url.URL)func(next http.Handler) http.Handler {
16+
returnfunc(next http.Handler) http.Handler {
17+
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
18+
origin:=r.Header.Get(httpmw.OriginHeader)
19+
originU,err:=url.Parse(origin)
20+
iferr!=nil {
21+
// TODO: Curl requests will not have this. One idea is to always show
22+
// html here??
23+
httpapi.Write(r.Context(),rw,http.StatusBadRequest, codersdk.Response{
24+
Message:"Internal error deleting OAuth2 client secret.",
25+
Detail:err.Error(),
26+
})
27+
return
28+
}
29+
30+
referer:=r.Referer()
31+
refererU,err:=url.Parse(referer)
32+
iferr!=nil {
33+
httpapi.Write(r.Context(),rw,http.StatusBadRequest, codersdk.Response{
34+
Message:"Internal error deleting OAuth2 client secret.",
35+
Detail:err.Error(),
36+
})
37+
return
38+
}
39+
40+
app:=httpmw.OAuth2ProviderApp(r)
41+
// If the request comes from outside, then we show the html allow page.
42+
// TODO: Skip this step if the user has already clicked allow before, and
43+
// we can just reuse the token.
44+
iforiginU.Hostname()!=accessURL.Hostname()&&refererU.Path!="/login/oauth2/authorize" {
45+
ifr.URL.Query().Get("redirected")!="" {
46+
site.RenderStaticErrorPage(rw,r, site.ErrorPageData{
47+
Status:http.StatusInternalServerError,
48+
HideStatus:false,
49+
Title:"Oauth Redirect Loop",
50+
Description:"Oauth redirect loop detected.",
51+
RetryEnabled:false,
52+
DashboardURL:accessURL.String(),
53+
Warnings:nil,
54+
})
55+
return
56+
}
57+
58+
redirect:=r.URL
59+
vals:=redirect.Query()
60+
vals.Add("redirected","true")
61+
r.URL.RawQuery=vals.Encode()
62+
site.RenderOAuthAllowPage(rw,r, site.RenderOAuthAllowData{
63+
AppName:app.Name,
64+
Icon:app.Icon,
65+
RedirectURI:r.URL.String(),
66+
})
67+
return
68+
}
69+
70+
next.ServeHTTP(rw,r)
71+
})
72+
}
73+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp