1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
|
package exampleop
import (
"crypto/sha256"
"log"
"log/slog"
"net/http"
"sync/atomic"
"time"
"github.com/go-chi/chi/v5"
"github.com/zitadel/logging"
"golang.org/x/text/language"
"github.com/zitadel/oidc/v3/pkg/op"
)
const (
pathLoggedOut = "/logged-out"
)
type Storage interface {
op.Storage
authenticate
deviceAuthenticate
}
// simple counter for request IDs
var counter atomic.Int64
// SetupServer creates an OIDC server with Issuer=http://localhost:<port>
//
// Use one of the pre-made clients in storage/clients.go or register a new one.
func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer bool, extraOptions ...op.Option) chi.Router {
// the OpenID Provider requires a 32-byte key for (token) encryption
// be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test"))
router := chi.NewRouter()
router.Use(logging.Middleware(
logging.WithLogger(logger),
logging.WithIDFunc(func() slog.Attr {
return slog.Int64("id", counter.Add(1))
}),
))
// for simplicity, we provide a very small default page for users who have signed out
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("signed out successfully"))
// no need to check/log error, this will be handled by the middleware.
})
// creation of the OpenIDProvider with the just created in-memory Storage
provider, err := newOP(
storage,
issuer,
key,
logger,
extraOptions...,
)
if err != nil {
log.Fatal(err)
}
//the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process
//for the simplicity of the example this means a simple page with username and password field
//be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage
l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest))
// regardless of how many pages / steps there are in the process, the UI must be registered in the router,
// so we will direct all calls to /login to the login UI
router.Mount("/login/", http.StripPrefix("/login", l.router))
router.Route("/device", func(r chi.Router) {
registerDeviceAuth(storage, r)
})
handler := http.Handler(provider)
if wrapServer {
handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints), op.AuthorizeCallbackHandler(provider))
}
// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
// is served on the correct path
//
// if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
// then you would have to set the path prefix (/custom/path/)
router.Mount("/", handler)
return router
}
// newOP will create an OpenID Provider for localhost on a specified port
// and a predefined default logout uri
// it will enable all options (see descriptions)
func newOP(
storage op.Storage,
issuer string,
key [32]byte, // encryption key
logger *slog.Logger,
extraOptions ...op.Option,
) (op.OpenIDProvider, error) {
config := &op.Config{
CryptoKey: 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: true,
// 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},
DeviceAuthorization: op.DeviceAuthorizationConfig{
Lifetime: 5 * time.Minute,
PollInterval: 5 * time.Second,
UserFormPath: "/device",
UserCode: op.UserCodeBase20,
},
}
handler, err := op.NewOpenIDProvider(issuer, config, storage,
append([]op.Option{
//we must explicitly allow the use of the http issuer
op.WithAllowInsecure(),
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
// Pass our logger to the OP
op.WithLogger(logger.WithGroup("op")),
}, extraOptions...)...,
)
if err != nil {
return nil, err
}
return handler, nil
}
|