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

Commite1365ae

Browse files
committed
Add OAuth2 auth, token, and revoke routes
1 parent7b7ba23 commite1365ae

File tree

8 files changed

+1183
-4
lines changed

8 files changed

+1183
-4
lines changed

‎codersdk/oauth2.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,48 @@ func (c *Client) DeleteOAuth2ProviderAppSecret(ctx context.Context, appID uuid.U
179179
}
180180
returnnil
181181
}
182+
183+
typeOAuth2ProviderGrantTypestring
184+
185+
const (
186+
OAuth2ProviderGrantTypeAuthorizationCodeOAuth2ProviderGrantType="authorization_code"
187+
)
188+
189+
func (eOAuth2ProviderGrantType)Valid()bool {
190+
switche {
191+
caseOAuth2ProviderGrantTypeAuthorizationCode:
192+
returntrue
193+
}
194+
returnfalse
195+
}
196+
197+
typeOAuth2ProviderResponseTypestring
198+
199+
const (
200+
OAuth2ProviderResponseTypeCodeOAuth2ProviderResponseType="code"
201+
)
202+
203+
func (eOAuth2ProviderResponseType)Valid()bool {
204+
switche {
205+
caseOAuth2ProviderResponseTypeCode:
206+
returntrue
207+
}
208+
returnfalse
209+
}
210+
211+
// RevokeOAuth2ProviderApp completely revokes an app's access for the user.
212+
func (c*Client)RevokeOAuth2ProviderApp(ctx context.Context,appID uuid.UUID)error {
213+
res,err:=c.Request(ctx,http.MethodDelete,"/login/oauth2/tokens",nil,func(r*http.Request) {
214+
q:=r.URL.Query()
215+
q.Set("client_id",appID.String())
216+
r.URL.RawQuery=q.Encode()
217+
})
218+
iferr!=nil {
219+
returnerr
220+
}
221+
deferres.Body.Close()
222+
ifres.StatusCode!=http.StatusNoContent {
223+
returnReadBodyAsError(res)
224+
}
225+
returnnil
226+
}

‎enterprise/coderd/coderd.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,28 @@ 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+
// Fetch the app as system because in the /tokens route there will be no
171+
// authenticated user.
172+
httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderApp(options.Database)),
173+
)
174+
// Oauth2 linking routes do not make sense under the /api/v2 path.
175+
r.Route("/login",func(r chi.Router) {
176+
r.Route("/oauth2",func(r chi.Router) {
177+
r.Group(func(r chi.Router) {
178+
r.Use(apiKeyMiddleware)
179+
r.Get("/authorize",api.postOAuth2ProviderAppAuthorize())
180+
r.Delete("/tokens",api.deleteOAuth2ProviderAppTokens())
181+
})
182+
// The /tokens endpoint will be called from an unauthorized client so we
183+
// cannot require an API key.
184+
r.Post("/tokens",api.postOAuth2ProviderAppToken())
185+
})
186+
})
187+
})
188+
167189
api.AGPL.APIHandler.Group(func(r chi.Router) {
168190
r.Get("/entitlements",api.serveEntitlements)
169191
// /regions overrides the AGPL /regions endpoint
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package identityprovider
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
"strings"
10+
"time"
11+
12+
"github.com/google/uuid"
13+
"golang.org/x/xerrors"
14+
15+
"github.com/coder/coder/v2/coderd/database"
16+
"github.com/coder/coder/v2/coderd/database/dbtime"
17+
"github.com/coder/coder/v2/coderd/httpapi"
18+
"github.com/coder/coder/v2/coderd/httpmw"
19+
"github.com/coder/coder/v2/codersdk"
20+
"github.com/coder/coder/v2/cryptorand"
21+
)
22+
23+
typeauthorizeParamsstruct {
24+
clientIDstring
25+
redirectURL*url.URL
26+
responseType codersdk.OAuth2ProviderResponseType
27+
scope []string
28+
statestring
29+
}
30+
31+
funcextractAuthorizeParams(r*http.Request,callbackURLstring) (authorizeParams, []codersdk.ValidationError,error) {
32+
p:=httpapi.NewQueryParamParser()
33+
vals:=r.URL.Query()
34+
35+
p.Required("state","response_type","client_id")
36+
37+
// TODO: Can we make this a URL straight out of the database?
38+
cb,err:=url.Parse(callbackURL)
39+
iferr!=nil {
40+
returnauthorizeParams{},nil,err
41+
}
42+
params:=authorizeParams{
43+
clientID:p.String(vals,"","client_id"),
44+
redirectURL:p.URL(vals,cb,"redirect_uri"),
45+
responseType:httpapi.ParseCustom(p,vals,"","response_type",httpapi.ParseEnum[codersdk.OAuth2ProviderResponseType]),
46+
scope:p.Strings(vals, []string{},"scope"),
47+
state:p.String(vals,"","state"),
48+
}
49+
50+
// We add "redirected" when coming from the authorize page.
51+
_=p.String(vals,"","redirected")
52+
53+
iferr:=validateRedirectURL(cb,params.redirectURL.String());err!=nil {
54+
p.Errors=append(p.Errors, codersdk.ValidationError{
55+
Field:"redirect_uri",
56+
Detail:fmt.Sprintf("Query param %q is invalid","redirect_uri"),
57+
})
58+
}
59+
60+
p.ErrorExcessParams(vals)
61+
returnparams,p.Errors,nil
62+
}
63+
64+
/**
65+
* Authorize displays an HTML for authorizing an application when the user has
66+
* first been redirected to this path and generates a code and redirects to the
67+
* app's callback URL after the user clicks "allow" on that page.
68+
*/
69+
funcAuthorize(db database.Store,accessURL*url.URL) http.HandlerFunc {
70+
handler:=func(rw http.ResponseWriter,r*http.Request) {
71+
ctx:=r.Context()
72+
apiKey:=httpmw.APIKey(r)
73+
app:=httpmw.OAuth2ProviderApp(r)
74+
75+
params,validationErrs,err:=extractAuthorizeParams(r,app.CallbackURL)
76+
iferr!=nil {
77+
httpapi.Write(r.Context(),rw,http.StatusInternalServerError, codersdk.Response{
78+
Message:"Failed to validate query parameters.",
79+
Detail:err.Error(),
80+
})
81+
return
82+
}
83+
iflen(validationErrs)>0 {
84+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
85+
Message:"Invalid query params.",
86+
Validations:validationErrs,
87+
})
88+
return
89+
}
90+
91+
// TODO: Ignoring scope for now, but should look into implementing.
92+
// 40 characters matches the length of GitHub's client secrets.
93+
rawCode,err:=cryptorand.String(40)
94+
iferr!=nil {
95+
httpapi.Write(r.Context(),rw,http.StatusInternalServerError, codersdk.Response{
96+
Message:"Failed to generate OAuth2 app authorization code.",
97+
})
98+
return
99+
}
100+
hashedCode:=Hash(rawCode,app.ID)
101+
err=db.InTx(func(tx database.Store)error {
102+
// Delete any previous codes.
103+
err=tx.DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx, database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams{
104+
AppID:app.ID,
105+
UserID:apiKey.UserID,
106+
})
107+
iferr!=nil&&!errors.Is(err,sql.ErrNoRows) {
108+
returnxerrors.Errorf("delete oauth2 app codes: %w",err)
109+
}
110+
111+
// Insert the new code.
112+
_,err=tx.InsertOAuth2ProviderAppCode(ctx, database.InsertOAuth2ProviderAppCodeParams{
113+
ID:uuid.New(),
114+
CreatedAt:dbtime.Now(),
115+
// TODO: Configurable expiration? Ten minutes matches GitHub.
116+
ExpiresAt:dbtime.Now().Add(time.Duration(10)*time.Minute),
117+
HashedSecret:hashedCode[:],
118+
AppID:app.ID,
119+
UserID:apiKey.UserID,
120+
})
121+
iferr!=nil {
122+
returnxerrors.Errorf("insert oauth2 authorization code: %w",err)
123+
}
124+
125+
returnnil
126+
},nil)
127+
iferr!=nil {
128+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
129+
Message:"Failed to generate OAuth2 authorization code.",
130+
Detail:err.Error(),
131+
})
132+
return
133+
}
134+
135+
newQuery:=params.redirectURL.Query()
136+
newQuery.Add("code",rawCode)
137+
newQuery.Add("state",params.state)
138+
params.redirectURL.RawQuery=newQuery.Encode()
139+
140+
http.Redirect(rw,r,params.redirectURL.String(),http.StatusTemporaryRedirect)
141+
}
142+
143+
// Always wrap with its custom mw.
144+
returnauthorizeMW(accessURL)(http.HandlerFunc(handler)).ServeHTTP
145+
}
146+
147+
// validateRedirectURL validates that the redirectURL is contained in baseURL.
148+
funcvalidateRedirectURL(baseURL*url.URL,redirectURLstring)error {
149+
redirect,err:=url.Parse(redirectURL)
150+
iferr!=nil {
151+
returnerr
152+
}
153+
// It can be a sub-directory but not a sub-domain, as we have apps on
154+
// sub-domains so it seems too dangerous.
155+
ifredirect.Host!=baseURL.Host||!strings.HasPrefix(redirect.Path,baseURL.Path) {
156+
returnxerrors.New("invalid redirect URL")
157+
}
158+
returnnil
159+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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:"Invalid or missing origin header.",
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:"Invalid or missing referer header.",
35+
Detail:err.Error(),
36+
})
37+
return
38+
}
39+
40+
app:=httpmw.OAuth2ProviderApp(r)
41+
ua:=httpmw.UserAuthorization(r)
42+
43+
// If the request comes from outside, then we show the html allow page.
44+
// TODO: Skip this step if the user has already clicked allow before, and
45+
// we can just reuse the token.
46+
iforiginU.Hostname()!=accessURL.Hostname()&&refererU.Path!="/login/oauth2/authorize" {
47+
ifr.URL.Query().Get("redirected")!="" {
48+
site.RenderStaticErrorPage(rw,r, site.ErrorPageData{
49+
Status:http.StatusInternalServerError,
50+
HideStatus:false,
51+
Title:"Oauth Redirect Loop",
52+
Description:"Oauth redirect loop detected.",
53+
RetryEnabled:false,
54+
DashboardURL:accessURL.String(),
55+
Warnings:nil,
56+
})
57+
return
58+
}
59+
60+
// Extract the form parameters for two reasons:
61+
// 1. We need the redirect URI to build the cancel URI.
62+
// 2. Since validation will run once the user clicks "allow", it is
63+
// better to validate now to avoid wasting the user's time clicking a
64+
// button that will just error anyway.
65+
params,errs,err:=extractAuthorizeParams(r,app.CallbackURL)
66+
iferr!=nil {
67+
site.RenderStaticErrorPage(rw,r, site.ErrorPageData{
68+
Status:http.StatusInternalServerError,
69+
HideStatus:false,
70+
Title:"Internal Server Error",
71+
Description:err.Error(),
72+
RetryEnabled:false,
73+
DashboardURL:accessURL.String(),
74+
Warnings:nil,
75+
})
76+
return
77+
}
78+
iflen(errs)>0 {
79+
errStr:=make([]string,len(errs))
80+
fori,err:=rangeerrs {
81+
errStr[i]=err.Detail
82+
}
83+
site.RenderStaticErrorPage(rw,r, site.ErrorPageData{
84+
Status:http.StatusBadRequest,
85+
HideStatus:false,
86+
Title:"Invalid Query Parameters",
87+
Description:"One or more query parameters are missing or invalid.",
88+
RetryEnabled:false,
89+
DashboardURL:accessURL.String(),
90+
Warnings:errStr,
91+
})
92+
return
93+
}
94+
95+
cancel:=params.redirectURL
96+
cancelQuery:=params.redirectURL.Query()
97+
cancelQuery.Add("error","access_denied")
98+
cancel.RawQuery=cancelQuery.Encode()
99+
100+
redirect:=r.URL
101+
vals:=redirect.Query()
102+
vals.Add("redirected","true")
103+
r.URL.RawQuery=vals.Encode()
104+
site.RenderOAuthAllowPage(rw,r, site.RenderOAuthAllowData{
105+
AppIcon:app.Icon,
106+
AppName:app.Name,
107+
CancelURI:cancel.String(),
108+
RedirectURI:r.URL.String(),
109+
Username:ua.ActorName,
110+
})
111+
return
112+
}
113+
114+
next.ServeHTTP(rw,r)
115+
})
116+
}
117+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp