Skip to content

Commit 04d12d9

Browse files
authored
Issue #52 refresh token endpoint (#55)
1 parent d33f166 commit 04d12d9

File tree

7 files changed

+111
-5
lines changed

7 files changed

+111
-5
lines changed

Diff for: app

21 MB
Binary file not shown.

Diff for: internal/api/auth/auth.go

+36
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ func (p *Provider) revokeToken(refreshToken string) error {
9898
return p.kc.revokeToken(refreshToken)
9999
}
100100

101+
func (p *Provider) refreshToken(refreshToken string) (*TokenRepr, error) {
102+
return p.kc.refreshToken(refreshToken)
103+
}
104+
101105
func GetLoginPageHandler(prov *Provider, logger *zap.Logger) gin.HandlerFunc {
102106
return func(c *gin.Context) {
103107
logger.Info("start to process login page request")
@@ -227,3 +231,35 @@ func PutLogoutHandler(prov *Provider, logger *zap.Logger) gin.HandlerFunc {
227231
c.Status(http.StatusNoContent)
228232
}
229233
}
234+
235+
type RefreshTokenReq struct {
236+
RefreshToken string `json:"refresh_token"`
237+
}
238+
239+
func PostRefreshHandler(prov *Provider, logger *zap.Logger) gin.HandlerFunc {
240+
return func(c *gin.Context) {
241+
logger.Info("start processing refresh token request")
242+
243+
var req RefreshTokenReq
244+
err := c.ShouldBindJSON(&req)
245+
if err != nil {
246+
apiErrors.RaiseBadRequestErr(c, apiErrors.ErrAuthMissingRefreshToken)
247+
return
248+
}
249+
250+
token, err := prov.refreshToken(req.RefreshToken)
251+
if err != nil {
252+
var keycloakErrorResponse KeycloakExternalError
253+
switch {
254+
case errors.As(err, &keycloakErrorResponse):
255+
apiErrors.RaiseBadRequestErr(c, keycloakErrorResponse)
256+
default:
257+
logger.Error("failed to refresh token", zap.Error(err))
258+
apiErrors.RaiseInternalErr(c, apiErrors.ErrAuthFailedRefreshToken)
259+
}
260+
261+
return
262+
}
263+
c.JSON(http.StatusOK, token)
264+
}
265+
}

Diff for: internal/api/auth/keycloak.go

+36
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,39 @@ func (kc *Keycloak) revokeToken(refreshToken string) error {
162162

163163
return nil
164164
}
165+
166+
func (kc *Keycloak) refreshToken(refreshToken string) (*TokenRepr, error) {
167+
data := url.Values{}
168+
data.Set("grant_type", "refresh_token")
169+
data.Set("client_id", kc.clientID)
170+
data.Set("client_secret", kc.clientSecret)
171+
data.Set("refresh_token", refreshToken)
172+
173+
req, err := http.NewRequest(http.MethodPost, kc.tokenURL, strings.NewReader(data.Encode())) //nolint:noctx
174+
if err != nil {
175+
return nil, err
176+
}
177+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
178+
179+
resp, err := kc.httpClient.Do(req)
180+
if err != nil {
181+
return nil, err
182+
}
183+
defer resp.Body.Close()
184+
185+
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
186+
var errResp KeycloakExternalError
187+
if err = json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
188+
return nil, err
189+
}
190+
191+
return nil, errResp
192+
}
193+
194+
var token TokenRepr
195+
if err = json.NewDecoder(resp.Body).Decode(&token); err != nil {
196+
return nil, err
197+
}
198+
199+
return &token, nil
200+
}

Diff for: internal/api/errors/auth.go

+2
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ var ErrAuthExchangeToken = errors.New("failed to exchange token")
1111
var ErrAuthWrongCodeVerifier = errors.New("failed to extract code verifier")
1212
var ErrAuthMissingDataForCodeVerifier = errors.New("missing data for code verifier")
1313
var ErrAuthMissingRefreshToken = errors.New("refresh token is missing")
14+
var ErrAuthFailedRefreshToken = errors.New("failed to refresh token")
15+
var ErrAuthExpiredRefreshToken = errors.New("refresh token has expired or is invalid")

Diff for: internal/api/errors/errors.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ func Return404(c *gin.Context) {
2929

3030
func RaiseInternalErr(c *gin.Context, err error) {
3131
intErr := fmt.Errorf("%w: %w", ErrInternalError, err)
32-
_ = c.AbortWithError(http.StatusInternalServerError, ReturnError(intErr))
32+
c.AbortWithStatusJSON(http.StatusInternalServerError, ReturnError(intErr))
3333
}
3434

3535
func RaiseBadRequestErr(c *gin.Context, err error) {
36-
_ = c.AbortWithError(http.StatusBadRequest, ReturnError(err))
36+
c.AbortWithStatusJSON(http.StatusBadRequest, ReturnError(err))
3737
}
3838

3939
func RaiseStatusNotFoundErr(c *gin.Context, err error) {
40-
_ = c.AbortWithError(http.StatusNotFound, ReturnError(err))
40+
c.AbortWithStatusJSON(http.StatusNotFound, ReturnError(err))
4141
}
4242

4343
func RaiseNotAuthorizedErr(c *gin.Context, err error) {
44-
_ = c.AbortWithError(http.StatusUnauthorized, ReturnError(err))
44+
c.AbortWithStatusJSON(http.StatusUnauthorized, ReturnError(err))
4545
}

Diff for: internal/api/routes.go

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func (a *API) InitRoutes() {
2020
authAPI.GET("callback", auth.GetCallbackHandler(a.oa2Prov, a.log))
2121
authAPI.POST("token", auth.PostTokenHandler(a.oa2Prov, a.log))
2222
authAPI.PUT("logout", auth.PutLogoutHandler(a.oa2Prov, a.log))
23+
authAPI.POST("refresh", auth.PostRefreshHandler(a.oa2Prov, a.log))
2324
}
2425

2526
v1API := a.r.Group(v1Group)

Diff for: openapi.yaml

+32-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,30 @@ paths:
111111
application/json:
112112
schema:
113113
$ref: '#/components/schemas/BadRequestGeneralError'
114-
114+
/auth/refresh:
115+
post:
116+
summary: Refresh an access token using a refresh token.
117+
tags:
118+
- authentication
119+
requestBody:
120+
content:
121+
application/json:
122+
schema:
123+
$ref: '#/components/schemas/TokenRefreshRequest'
124+
required: true
125+
responses:
126+
'200':
127+
description: Return new access and refresh tokens.
128+
content:
129+
application/json:
130+
schema:
131+
$ref: '#/components/schemas/TokenPostResponse'
132+
'400':
133+
description: The request is invalid.
134+
content:
135+
application/json:
136+
schema:
137+
$ref: '#/components/schemas/BadRequestGeneralError'
115138
/v2/components:
116139
get:
117140
summary: Get all components.
@@ -358,6 +381,14 @@ components:
358381
properties:
359382
refresh_token:
360383
type: string
384+
TokenRefreshRequest:
385+
type: object
386+
required:
387+
- refresh_token
388+
properties:
389+
refresh_token:
390+
type: string
391+
example: "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjOTBjZjFhOC1kNjcxLTRjNzktYTBlYi0yYmI3Y2M2NThiNzIifQ..."
361392
Component:
362393
type: object
363394
required:

0 commit comments

Comments
 (0)