11 "github.com/MicahParks/keyfunc/v3"
12 "github.com/golang-jwt/jwt/v5"
15 var openid_configuration
struct {
16 AuthorizationEndpoint
string `json:"authorization_endpoint"`
17 TokenEndpoint
string `json:"token_endpoint"`
18 JwksUri
string `json:"jwks_uri"`
19 UserinfoEndpoint
string `json:"userinfo_endpoint"`
22 var openid_keyfunc keyfunc
.Keyfunc
24 type msclaims_t
struct {
25 /* TODO: These may be non-portable Microsoft attributes */
26 Name
string `json:"name"` /* Scope: profile */
27 Email
string `json:"email"` /* Scope: email */
32 * Fetch the OpenID Connect configuration. The endpoint specified in
33 * the configuration is incomplete and we fetch the OpenID Connect
34 * configuration from the well-known endpoint.
35 * This seems to be supported by many authentication providers.
36 * The following work, as config.Openid.Endpoint:
37 * - https://login.microsoftonline.com/common
38 * - https://accounts.google.com/.well-known/openid-configuration
40 func get_openid_config(endpoint
string) {
41 resp
, err
:= http
.Get(endpoint
+ "/.well-known/openid-configuration")
43 defer resp
.Body
.Close()
44 if resp
.StatusCode
!= 200 {
45 log
.Fatal(fmt
.Sprintf(
46 "Got response code %d from openid-configuration\n",
50 err
= json
.NewDecoder(resp
.Body
).Decode(&openid_configuration
)
53 resp
, err
= http
.Get(openid_configuration
.JwksUri
)
55 defer resp
.Body
.Close()
56 if resp
.StatusCode
!= 200 {
57 log
.Fatal(fmt
.Sprintf(
58 "Got response code %d from JwksUri\n",
63 if config
.Openid
.Authorize
!= "" {
64 openid_configuration
.AuthorizationEndpoint
=
65 config
.Openid
.Authorize
68 jwks_json
, err
:= io
.ReadAll(resp
.Body
)
72 * TODO: The key set is never updated, which is technically incorrect.
73 * We could use keyfunc's auto-update mechanism, but I'd prefer
74 * controlling when to do it manually. Remember to wrap it around a
75 * mutex or some semaphores though.
77 openid_keyfunc
, err
= keyfunc
.NewJWKSetJSON(jwks_json
)
81 func generate_authorization_url() string {
83 * TODO: Handle nonces and anti-replay. Incremental nonces would be
84 * nice on memory and speed (depending on how maps are implemented in
85 * Go, hopefully it's some sort of btree), but that requires either
86 * hacky atomics or having a multiple goroutines to handle
87 * authentication, neither of which are desirable.
93 "&response_type=id_token"+
94 "&redirect_uri=%s/oidc"+
95 "&response_mode=form_post"+
96 "&scope=openid+profile+email"+
98 openid_configuration
.AuthorizationEndpoint
,
105 func handle_oidc(w http
.ResponseWriter
, req
*http
.Request
) {
106 if req
.Method
!= "POST" {
107 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
111 "Only POST is allowed on the OIDC callback.\n" +
112 "Please return to the login page and retry.\n",
117 err
:= req
.ParseForm()
119 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
123 "Malformed form data.\n",
128 returned_error
:= req
.PostFormValue("error")
129 if returned_error
!= "" {
130 returned_error_description
:=
131 req
.PostFormValue("error_description")
132 if returned_error_description
== "" {
135 "text/plain; charset=utf-8",
138 w
.Write([]byte(fmt
.Sprintf(
146 "text/plain; charset=utf-8",
149 w
.Write([]byte(fmt
.Sprintf(
152 returned_error_description
,
158 id_token_string
:= req
.PostFormValue("id_token")
159 if id_token_string
== "" {
160 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
162 w
.Write([]byte(fmt
.Sprintf("Error\nMissing id_token.\n")))
166 token
, err
:= jwt
.ParseWithClaims(
169 openid_keyfunc
.Keyfunc
,
172 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
174 w
.Write([]byte(fmt
.Sprintf("Error\nCannot parse claims.\n")))
181 case errors
.Is(err
, jwt
.ErrTokenMalformed
):
182 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
184 w
.Write([]byte(fmt
.Sprintf("Error\nMalformed JWT token.\n")))
186 case errors
.Is(err
, jwt
.ErrTokenSignatureInvalid
):
187 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
189 w
.Write([]byte(fmt
.Sprintf("Error\nInvalid JWS signature.\n")))
191 case errors
.Is(err
, jwt
.ErrTokenExpired
) ||
192 errors
.Is(err
, jwt
.ErrTokenNotValidYet
):
193 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
195 w
.Write([]byte(fmt
.Sprintf(
197 "JWT token expired or not yet valid.\n",
201 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
203 w
.Write([]byte(fmt
.Sprintf("Error\nInvalid JWT token.\n")))
207 claims
, claims_ok
:= token
.Claims
.(*msclaims_t
)
210 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
212 w
.Write([]byte(fmt
.Sprintf("Error\nCannot unpack claims.\n")))
216 cookie_value
:= random(20)
218 cookie
:= http
.Cookie
{
221 SameSite
: http
.SameSiteLaxMode
,
225 http
.SetCookie(w
, &cookie
)
226 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
228 w
.Write([]byte(fmt
.Sprintf(
229 "Name: %s\nEmail: %s\nSubject: %s\nCookie: %s\n",