Skip to content

Commit 9a6676b

Browse files
committed
feat: add pkce support to oidc server
1 parent 431cd33 commit 9a6676b

10 files changed

Lines changed: 126 additions & 35 deletions

File tree

frontend/src/lib/hooks/oidc.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export type OIDCValues = {
55
redirect_uri: string;
66
state: string;
77
nonce: string;
8+
code_challenge: string;
9+
code_challenge_method: string;
810
};
911

1012
interface IuseOIDCParams {
@@ -14,7 +16,12 @@ interface IuseOIDCParams {
1416
missingParams: string[];
1517
}
1618

17-
const optionalParams: string[] = ["state", "nonce"];
19+
const optionalParams: string[] = [
20+
"state",
21+
"nonce",
22+
"code_challenge",
23+
"code_challenge_method",
24+
];
1825

1926
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
2027
let compiled: string = "";
@@ -28,6 +35,8 @@ export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
2835
redirect_uri: params.get("redirect_uri") ?? "",
2936
state: params.get("state") ?? "",
3037
nonce: params.get("nonce") ?? "",
38+
code_challenge: params.get("code_challenge") ?? "",
39+
code_challenge_method: params.get("code_challenge_method") ?? "",
3140
};
3241

3342
for (const key of Object.keys(values)) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "oidc_codes" DROP COLUMN "code_challenge";
2+
ALTER TABLE "oidc_codes" DROP COLUMN "code_challenge_method";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "oidc_codes" ADD COLUMN "code_challenge" TEXT DEFAULT "";
2+
ALTER TABLE "oidc_codes" ADD COLUMN "code_challenge_method" TEXT DEFAULT "";

internal/controller/oidc_controller.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type TokenRequest struct {
3434
RefreshToken string `form:"refresh_token" url:"refresh_token"`
3535
ClientSecret string `form:"client_secret" url:"client_secret"`
3636
ClientID string `form:"client_id" url:"client_id"`
37+
CodeVerifier string `form:"code_verifier" url:"code_verifier"`
3738
}
3839

3940
type CallbackError struct {
@@ -308,6 +309,16 @@ func (controller *OIDCController) Token(c *gin.Context) {
308309
return
309310
}
310311

312+
ok := controller.oidc.ValidatePKCE(entry.CodeChallenge, entry.CodeChallengeMethod, req.CodeVerifier)
313+
314+
if !ok {
315+
tlog.App.Warn().Msg("PKCE validation failed")
316+
c.JSON(400, gin.H{
317+
"error": "invalid_grant",
318+
})
319+
return
320+
}
321+
311322
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry)
312323

313324
if err != nil {

internal/repository/models.go

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/repository/oidc_queries.sql.go

Lines changed: 33 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/service/oidc_service.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@ type TokenResponse struct {
7575
}
7676

7777
type AuthorizeRequest struct {
78-
Scope string `json:"scope" binding:"required"`
79-
ResponseType string `json:"response_type" binding:"required"`
80-
ClientID string `json:"client_id" binding:"required"`
81-
RedirectURI string `json:"redirect_uri" binding:"required"`
82-
State string `json:"state"`
83-
Nonce string `json:"nonce"`
78+
Scope string `json:"scope" binding:"required"`
79+
ResponseType string `json:"response_type" binding:"required"`
80+
ClientID string `json:"client_id" binding:"required"`
81+
RedirectURI string `json:"redirect_uri" binding:"required"`
82+
State string `json:"state"`
83+
Nonce string `json:"nonce"`
84+
CodeChallenge string `json:"code_challenge"`
85+
CodeChallengeMethod string `json:"code_challenge_method"`
8486
}
8587

8688
type OIDCServiceConfig struct {
@@ -293,6 +295,13 @@ func (service *OIDCService) ValidateAuthorizeParams(req AuthorizeRequest) error
293295
return errors.New("invalid_request_uri")
294296
}
295297

298+
// PKCE code challenge method if set
299+
if req.CodeChallenge != "" && req.CodeChallengeMethod != "" {
300+
if req.CodeChallengeMethod != "S256" || req.CodeChallenge == "plain" {
301+
return errors.New("invalid_request")
302+
}
303+
}
304+
296305
return nil
297306
}
298307

@@ -306,8 +315,7 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
306315
// Fixed 10 minutes
307316
expiresAt := time.Now().Add(time.Minute * time.Duration(10)).Unix()
308317

309-
// Insert the code into the database
310-
_, err := service.queries.CreateOidcCode(c, repository.CreateOidcCodeParams{
318+
entry := repository.CreateOidcCodeParams{
311319
Sub: sub,
312320
CodeHash: service.Hash(code),
313321
// Here it's safe to split and trust the output since, we validated the scopes before
@@ -316,7 +324,21 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
316324
ClientID: req.ClientID,
317325
ExpiresAt: expiresAt,
318326
Nonce: req.Nonce,
319-
})
327+
}
328+
329+
if req.CodeChallenge != "" {
330+
if req.CodeChallengeMethod == "S256" {
331+
entry.CodeChallenge = req.CodeChallenge
332+
entry.CodeChallengeMethod = "S256"
333+
} else {
334+
entry.CodeChallenge = service.hashAndEncodePKCE(req.CodeChallenge)
335+
entry.CodeChallengeMethod = "plain"
336+
tlog.App.Warn().Msg("Received plain PKCE code challenge, it's recommended to use S256 for better security")
337+
}
338+
}
339+
340+
// Insert the code into the database
341+
_, err := service.queries.CreateOidcCode(c, entry)
320342

321343
return err
322344
}
@@ -728,3 +750,20 @@ func (service *OIDCService) GetJWK() ([]byte, error) {
728750

729751
return jwk.Public().MarshalJSON()
730752
}
753+
754+
func (service *OIDCService) ValidatePKCE(codeChallenge string, codeChallengeMethod string, codeVerifier string) bool {
755+
if codeChallenge == "" {
756+
return true
757+
}
758+
if codeChallengeMethod == "plain" {
759+
// Code challenge is hashed and encoded in the database for security reasons
760+
return codeChallenge == service.hashAndEncodePKCE(codeVerifier)
761+
}
762+
return codeChallenge == codeVerifier
763+
}
764+
765+
func (service *OIDCService) hashAndEncodePKCE(codeVerifier string) string {
766+
hasher := sha256.New()
767+
hasher.Write([]byte(codeVerifier))
768+
return base64.URLEncoding.EncodeToString(hasher.Sum(nil))
769+
}

sql/oidc_queries.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ INSERT INTO "oidc_codes" (
66
"redirect_uri",
77
"client_id",
88
"expires_at",
9-
"nonce"
9+
"nonce",
10+
"code_challenge",
11+
"code_challenge_method"
1012
) VALUES (
11-
?, ?, ?, ?, ?, ?, ?
13+
?, ?, ?, ?, ?, ?, ?, ?, ?
1214
)
1315
RETURNING *;
1416

sql/oidc_schemas.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ CREATE TABLE IF NOT EXISTS "oidc_codes" (
55
"redirect_uri" TEXT NOT NULL,
66
"client_id" TEXT NOT NULL,
77
"expires_at" INTEGER NOT NULL,
8-
"nonce" TEXT DEFAULT ""
8+
"nonce" TEXT DEFAULT "",
9+
"code_challenge" TEXT DEFAULT "",
10+
"code_challenge_method" TEXT DEFAULT ""
911
);
1012

1113
CREATE TABLE IF NOT EXISTS "oidc_tokens" (

sqlc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ sql:
2626
go_type: "string"
2727
- column: "oidc_tokens.nonce"
2828
go_type: "string"
29+
- column: "oidc_codes.code_challenge"
30+
go_type: "string"
31+
- column: "oidc_codes.code_challenge_method"
32+
go_type: "string"

0 commit comments

Comments
 (0)