Skip to content
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

Application Integration: OIDC endpoints #869

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add tests and fix most major bugs
First full runthrough is implemented as a test. Most functionality should work now.
Major errors are eliminated.
  • Loading branch information
FerdinandvHagen committed Jun 26, 2023
commit 1c18cb21f5e11f11be523d7e3c6e65d15a3f6aeb
1 change: 1 addition & 0 deletions backend/config/config.go
Original file line number Diff line number Diff line change
@@ -612,6 +612,7 @@ type OIDCClient struct {
type OIDC struct {
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
Issuer string `yaml:"issuer" json:"issuer" koanf:"issuer"`
Key string `yaml:"key" json:"key" koanf:"key"`
Clients []OIDCClient `yaml:"clients" json:"clients" koanf:"clients"`
}

3 changes: 1 addition & 2 deletions backend/config/config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
database:
user: hanko
password: hanko
user: postgres
host: localhost
port: 5432
dialect: postgres
144 changes: 144 additions & 0 deletions backend/handler/oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package handler

import (
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt"
auditlog "github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/handler/oidc"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/session"
"github.com/zitadel/oidc/v2/pkg/op"
"golang.org/x/text/language"
"net/http"
)

type OIDCHandler struct {
cfg *config.Config
persister persistence.Persister
sessionManager session.Manager
auditLogger auditlog.Logger
provider op.OpenIDProvider
}

func NewOIDCHandler(
cfg *config.Config,
persister persistence.Persister,
sessionManager session.Manager,
auditLogger auditlog.Logger,
) *OIDCHandler {
if !cfg.OIDC.Enabled {
return nil
}

key, err := base64.URLEncoding.DecodeString(cfg.OIDC.Key)
if err != nil {
panic(err)
}

if len(key) != 32 {
panic("key must be 32 bytes long")
}

pathLoggedOut := "/logged_out"

var extraOptions []op.Option

config := &op.Config{
CryptoKey: [32]byte(key),

// will be used if the end_session endpoint is called without a post_logout_redirect_uri
DefaultLogoutRedirectURI: pathLoggedOut,

// enables code_challenge_method S256 for PKCE (and therefore PKCE in general)
CodeMethodS256: true,

// enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth)
AuthMethodPost: true,

// enables additional authentication by using private_key_jwt
AuthMethodPrivateKeyJWT: false,

// enables refresh_token grant use
GrantTypeRefreshToken: true,

// enables use of the `request` Object parameter
RequestObjectSupported: true,

// this example has only static texts (in English), so we'll set the here accordingly
SupportedUILocales: []language.Tag{language.English},
}

storage := oidc.NewStorage(persister)
for _, client := range cfg.OIDC.Clients {
err := storage.AddClient(&client)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

39% of developers fix this issue

G601: Implicit memory aliasing in for loop.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.

if err != nil {
panic(err)
}
}

provider, err := op.NewOpenIDProvider(cfg.OIDC.Issuer, config, storage, append([]op.Option{
op.WithCustomEndpoints(
op.NewEndpoint("/oauth/authorize"),
op.NewEndpoint("/oauth/token"),
op.NewEndpoint("/oauth/userinfo"),
op.NewEndpoint("/oauth/revoke"),
op.NewEndpoint("/oauth/end_session"),
op.NewEndpoint("/oauth/keys"),
),
op.WithCustomDeviceAuthorizationEndpoint(op.NewEndpoint("/oauth/device_authorization")),
}, extraOptions...)...)
if err != nil {
panic(err)
}

fmt.Println("OIDC provider initialized")
f := op.AuthCallbackURL(provider)
fmt.Println("OIDC callback url:", f(context.Background(), "testID"))
fmt.Println("OIDC callback url:")

return &OIDCHandler{
cfg: cfg,
persister: persister,
sessionManager: sessionManager,
auditLogger: auditLogger,
provider: provider,
}
}

func (h *OIDCHandler) Handler(c echo.Context) error {
h.provider.HttpHandler().ServeHTTP(c.Response(), c.Request())

return nil
}

func (h *OIDCHandler) LoginHandler(c echo.Context) error {
sessionToken, ok := c.Get("session").(jwt.Token)
if !ok {
return errors.New("failed to cast session object")
}

authRequestID := c.QueryParam("id")
if authRequestID == "" {
return c.String(400, "id parameter missing")
}

uid, err := uuid.FromString(authRequestID)
if err != nil {
return c.String(400, "id parameter invalid")
}

persister := h.persister.GetOIDCAuthRequestPersister()

err = persister.AuthorizeUser(c.Request().Context(), uid, sessionToken.Subject())
if err != nil {
return c.String(500, "error authorizing user")
}

return c.Redirect(http.StatusFound, "/oauth/authorize/callback?id="+authRequestID)
}
2 changes: 1 addition & 1 deletion backend/oidc/client.go → backend/handler/oidc/client.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package oidc

import (
"github.com/zitadel/oidc/v2/pkg/oidc"
34 changes: 19 additions & 15 deletions backend/oidc/oidc.go → backend/handler/oidc/oidc.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package main
package oidc

import (
"errors"
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language"
"strings"
"time"
)

@@ -24,9 +25,8 @@ type AuthRequest struct {
ResponseType oidc.ResponseType
Nonce string
CodeChallenge string

done bool
authTime time.Time
LoginDone bool
AuthTime time.Time
}

func NewAuthRequestFromModel(request *models.AuthRequest) (*AuthRequest, error) {
@@ -35,25 +35,29 @@ func NewAuthRequestFromModel(request *models.AuthRequest) (*AuthRequest, error)
}

var uiLocales []language.Tag
for _, tag := range request.UILocales {
for _, tag := range request.GetUILocales() {
uiLocales = append(uiLocales, language.Make(tag))
}

maxAuthAge := request.GetMaxAuthAge()

return &AuthRequest{
ID: request.ID,
CreationDate: request.CreatedAt,
ApplicationID: request.ClientID,
CallbackURI: request.CallbackURI,
TransferState: request.TransferState,
Prompt: request.Prompt,
Prompt: request.GetPrompt(),
UiLocales: uiLocales,
LoginHint: request.LoginHint,
MaxAuthAge: &request.MaxAuthAge,
MaxAuthAge: &maxAuthAge,
UserID: request.UserID,
Scopes: request.Scopes,
Scopes: request.GetScopes(),
ResponseType: oidc.ResponseType(request.ResponseType),
Nonce: request.Nonce,
CodeChallenge: request.CodeChallenge,
LoginDone: request.Done,
AuthTime: request.AuthTime,
}, nil
}

@@ -69,7 +73,7 @@ func (a *AuthRequest) GetAMR() []string {
// TODO: https://www.rfc-editor.org/rfc/rfc8176.html

// this example only uses password for authentication
if a.done {
if a.LoginDone {
return []string{"pwd"}
}
return nil
@@ -80,7 +84,7 @@ func (a *AuthRequest) GetAudience() []string {
}

func (a *AuthRequest) GetAuthTime() time.Time {
return a.authTime
return a.AuthTime
}

func (a *AuthRequest) GetClientID() string {
@@ -123,7 +127,7 @@ func (a *AuthRequest) GetSubject() string {
}

func (a *AuthRequest) Done() bool {
return a.done
return a.LoginDone
}

func (a *AuthRequest) ToModel() models.AuthRequest {
@@ -143,12 +147,12 @@ func (a *AuthRequest) ToModel() models.AuthRequest {
ClientID: a.ApplicationID,
CallbackURI: a.CallbackURI,
TransferState: a.TransferState,
Prompt: a.Prompt,
UILocales: locales,
Prompt: strings.Join(a.Prompt, ","),
UILocales: strings.Join(locales, ","),
LoginHint: a.LoginHint,
MaxAuthAge: maxAuthAge,
MaxAuthAge: int64(maxAuthAge.Seconds()),
UserID: a.UserID,
Scopes: a.Scopes,
Scopes: strings.Join(a.Scopes, ","),
ResponseType: string(a.ResponseType),
Nonce: a.Nonce,
CodeChallenge: a.CodeChallenge,
Loading