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 TokenEndpointAuthMethodsSupported ([]string) `json:"token_endpoint_auth_methods_supported"`
19 JwksUri
string `json:"jwks_uri"`
20 UserinfoEndpoint
string `json:"userinfo_endpoint"`
23 var openid_keyfunc keyfunc
.Keyfunc
25 type msclaims_t
struct {
26 /* TODO: These may be non-portable Microsoft attributes */
27 Name
string `json:"name"` /* Scope: profile */
28 Email
string `json:"email"` /* Scope: email */
33 * Fetch the OpenID Connect configuration. The endpoint specified in
34 * the configuration is incomplete and we fetch the OpenID Connect
35 * configuration from the well-known endpoint.
36 * This seems to be supported by many authentication providers.
37 * The following work, as config.Openid.Endpoint:
38 * - https://login.microsoftonline.com/common
39 * - https://accounts.google.com/.well-known/openid-configuration
41 func get_openid_config(endpoint
string) {
42 resp
, err
:= http
.Get(endpoint
+ "/.well-known/openid-configuration")
44 defer resp
.Body
.Close()
45 if resp
.StatusCode
!= 200 {
46 log
.Fatal(fmt
.Sprintf(
47 "Got response code %d from openid-configuration\n",
51 err
= json
.NewDecoder(resp
.Body
).Decode(&openid_configuration
)
54 resp
, err
= http
.Get(openid_configuration
.JwksUri
)
56 defer resp
.Body
.Close()
57 if resp
.StatusCode
!= 200 {
58 log
.Fatal(fmt
.Sprintf(
59 "Got response code %d from JwksUri\n",
64 if config
.Openid
.Authorize
!= "" {
65 openid_configuration
.AuthorizationEndpoint
= 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.
91 "%s?client_id=%s&response_type=id_token&redirect_uri=%s%s&response_mode=form_post&scope=openid+profile+email&nonce=%s",
92 openid_configuration
.AuthorizationEndpoint
,
95 config
.Openid
.Redirect
,
100 func handle_oidc(w http
.ResponseWriter
, req
*http
.Request
) {
101 if req
.Method
!= "POST" {
102 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
104 w
.Write([]byte("Error: The OpenID Connect authorization endpoint only accepts POST requests.\n"))
108 err
:= req
.ParseForm()
110 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
112 w
.Write([]byte("Error: Malformed form data.\n"))
116 returned_error
:= req
.PostFormValue("error")
117 if returned_error
!= "" {
118 returned_error_description
:= req
.PostFormValue("error_description")
119 if returned_error_description
== "" {
120 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
122 w
.Write([]byte(fmt
.Sprintf(
123 "Error: The OpenID Connect callback returned an error %s, but did not provide an error_description.\n",
128 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
130 w
.Write([]byte(fmt
.Sprintf(
131 "Error: The OpenID Connect callback returned an error:\n\n%s\n\n%s\n",
133 returned_error_description
,
139 id_token_string
:= req
.PostFormValue("id_token")
140 if id_token_string
== "" {
141 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
143 w
.Write([]byte(fmt
.Sprintf("Error: The OpenID Connect callback did not return an error, but no id_token was found.\n")))
147 fmt
.Println(id_token_string
)
149 token
, err
:= jwt
.ParseWithClaims(
152 openid_keyfunc
.Keyfunc
,
155 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
157 w
.Write([]byte(fmt
.Sprintf("Error: Error parsing JWT with custom claims.\n")))
164 case errors
.Is(err
, jwt
.ErrTokenMalformed
):
165 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
167 w
.Write([]byte(fmt
.Sprintf("Error: Malformed JWT token.\n")))
169 case errors
.Is(err
, jwt
.ErrTokenSignatureInvalid
):
170 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
172 w
.Write([]byte(fmt
.Sprintf("Error: Invalid signature on JWT token.\n")))
174 case errors
.Is(err
, jwt
.ErrTokenExpired
) || errors
.Is(err
, jwt
.ErrTokenNotValidYet
):
175 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
177 w
.Write([]byte(fmt
.Sprintf("Error: JWT token expired or not yet valid.\n")))
180 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
182 w
.Write([]byte(fmt
.Sprintf("Error: Funny JWT token.\n")))
186 claims
, claims_ok
:= token
.Claims
.(*msclaims_t
)
189 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
191 w
.Write([]byte(fmt
.Sprintf("Error: JWT token's claims are not OK.\n")))
195 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
197 w
.Write([]byte(fmt
.Sprintf("Name: %s\nEmail: %s\nSubject: %s\n", claims
.Name
, claims
.Email
, claims
.Subject
)))