Skip to content

Commit 6bffc22

Browse files
committed
feat: support OIDC logout with Coder logout
1 parent 8da568b commit 6bffc22

File tree

5 files changed

+135
-1
lines changed

5 files changed

+135
-1
lines changed

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,7 @@ func New(options *Options) *API {
11361136
r.Post("/", api.postUser)
11371137
r.Get("/", api.users)
11381138
r.Post("/logout", api.postLogout)
1139+
r.Get("/oidc-logout", api.userOIDCLogoutURL)
11391140
// These routes query information about site wide roles.
11401141
r.Route("/roles", func(r chi.Router) {
11411142
r.Get("/", api.AssignableSiteRoles)

coderd/userauth.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"net/http"
99
"net/mail"
10+
"net/url"
1011
"sort"
1112
"strconv"
1213
"strings"
@@ -743,6 +744,95 @@ func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) {
743744
})
744745
}
745746

747+
// Returns the OIDC logout URL.
748+
//
749+
// @Summary Returns URL for the OIDC logout
750+
// @ID user-oidc-logout
751+
// @Security CoderSessionToken
752+
// @Produce json
753+
// @Tags Users
754+
// @Success 200 {object} map[string]string "Returns a map containing the OIDC logout URL"
755+
// @Router /users/oidc-logout [get]
756+
func (api *API) userOIDCLogoutURL(rw http.ResponseWriter, r *http.Request) {
757+
ctx := r.Context()
758+
759+
// Get logged-in user
760+
apiKey := httpmw.APIKey(r)
761+
user, err := api.Database.GetUserByID(ctx, apiKey.UserID)
762+
if err != nil {
763+
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
764+
Message: "Failed to retrieve user information.",
765+
})
766+
return
767+
}
768+
769+
// Retrieve the user's OAuthAccessToken for logout
770+
// nolint:gocritic // We only can get user link by user ID and login type with the system auth.
771+
link, err := api.Database.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx),
772+
database.GetUserLinkByUserIDLoginTypeParams{
773+
UserID: user.ID,
774+
LoginType: user.LoginType,
775+
})
776+
if err != nil {
777+
api.Logger.Error(ctx, "failed to retrieve OIDC user link", "error", err)
778+
if xerrors.Is(err, sql.ErrNoRows) {
779+
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
780+
Message: "No OIDC link found for this user.",
781+
})
782+
} else {
783+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
784+
Message: "Failed to retrieve user authentication data.",
785+
})
786+
}
787+
return
788+
}
789+
790+
rawIDToken := link.OAuthAccessToken
791+
792+
// Retrieve OIDC environment variables
793+
dvOIDC := api.DeploymentValues.OIDC
794+
oidcEndpoint := dvOIDC.LogoutEndpoint.Value()
795+
oidcClientID := dvOIDC.ClientID.Value()
796+
logoutURI := dvOIDC.LogoutRedirectURI.Value()
797+
798+
if oidcEndpoint == "" {
799+
api.Logger.Error(ctx, "missing OIDC logout endpoint")
800+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
801+
Message: "OIDC configuration is missing.",
802+
})
803+
return
804+
}
805+
806+
// Construct OIDC Logout URL
807+
logoutURL, err := url.Parse(oidcEndpoint)
808+
if err != nil {
809+
api.Logger.Error(ctx, "failed to parse OIDC endpoint", "error", err)
810+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
811+
Message: "Invalid OIDC endpoint.",
812+
})
813+
return
814+
}
815+
816+
// Build parameters
817+
q := url.Values{}
818+
819+
if oidcClientID != "" {
820+
q.Set("client_id", oidcClientID)
821+
}
822+
if rawIDToken != "" {
823+
q.Set("id_token_hint", rawIDToken)
824+
}
825+
if logoutURI != "" {
826+
q.Set("logout_uri", logoutURI)
827+
}
828+
829+
logoutURL.RawQuery = q.Encode()
830+
831+
// Return full logout URL
832+
response := map[string]string{"oidc_logout_url": logoutURL.String()}
833+
httpapi.Write(ctx, rw, http.StatusOK, response)
834+
}
835+
746836
// GithubOAuth2Team represents a team scoped to an organization.
747837
type GithubOAuth2Team struct {
748838
Organization string

codersdk/deployment.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,8 @@ type OIDCConfig struct {
555555
IconURL serpent.URL `json:"icon_url" typescript:",notnull"`
556556
SignupsDisabledText serpent.String `json:"signups_disabled_text" typescript:",notnull"`
557557
SkipIssuerChecks serpent.Bool `json:"skip_issuer_checks" typescript:",notnull"`
558+
LogoutEndpoint serpent.String `json:"logout_endpoint" typescript:",notnull"`
559+
LogoutRedirectURI serpent.String `json:"logout_redirect_uri" typescript:",notnull"`
558560
}
559561

560562
type TelemetryConfig struct {
@@ -1966,6 +1968,26 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
19661968
Group: &deploymentGroupOIDC,
19671969
YAML: "dangerousSkipIssuerChecks",
19681970
},
1971+
{
1972+
Name: "OIDC logout endpoint",
1973+
Description: "OIDC endpoint for logout.",
1974+
Flag: "logout-endpoint",
1975+
Env: "CODER_OIDC_LOGOUT_ENDPOINT",
1976+
Default: "",
1977+
Value: &c.OIDC.LogoutEndpoint,
1978+
Group: &deploymentGroupOIDC,
1979+
YAML: "logoutEndpoint",
1980+
},
1981+
{
1982+
Name: "OIDC logout redirect URI",
1983+
Description: "OIDC redirect URI after logout.",
1984+
Flag: "logout-redirect-uri",
1985+
Env: "CODER_OIDC_LOGOUT_URI",
1986+
Default: "",
1987+
Value: &c.OIDC.LogoutRedirectURI,
1988+
Group: &deploymentGroupOIDC,
1989+
YAML: "logoutRedirectURI",
1990+
},
19691991
// Telemetry settings
19701992
telemetryEnable,
19711993
{

site/src/api/api.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,26 @@ class ApiMethods {
457457
};
458458

459459
logout = async (): Promise<void> => {
460-
return this.axios.post("/api/v2/users/logout");
460+
try {
461+
// Fetch the stored ID token from the backend
462+
const response = await this.axios.get("/api/v2/users/oidc-logout");
463+
464+
// Redirect to OIDC logout after Coder logout
465+
if (response.data.oidc_logout_url) {
466+
// Coder session logout
467+
await this.axios.post("/api/v2/users/logout");
468+
469+
// OIDC logout
470+
window.location.href = response.data.oidc_logout_url;
471+
} else {
472+
// Redirect normally if no token is available
473+
console.warn("No ID token found, continuing logout without OIDC logout.");
474+
return this.axios.post("/api/v2/users/logout");
475+
}
476+
} catch (error) {
477+
console.error("Logout failed", error);
478+
return this.axios.post("/api/v2/users/logout");
479+
}
461480
};
462481

463482
getAuthenticatedUser = async () => {

site/src/api/typesGenerated.ts

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

0 commit comments

Comments
 (0)