Add configuration example
[fbfp.git] / oidc.go
blobde60182bb8a1e73cdf8de0a7b02b7b92dffabfeb
1 package main
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "log"
9 "net/http"
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 */
29 jwt.RegisteredClaims
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")
43 e(err)
44 defer resp.Body.Close()
45 if resp.StatusCode != 200 {
46 log.Fatal(fmt.Sprintf(
47 "Got response code %d from openid-configuration\n",
48 resp.StatusCode,
51 err = json.NewDecoder(resp.Body).Decode(&openid_configuration)
52 e(err)
54 resp, err = http.Get(openid_configuration.JwksUri)
55 e(err)
56 defer resp.Body.Close()
57 if resp.StatusCode != 200 {
58 log.Fatal(fmt.Sprintf(
59 "Got response code %d from JwksUri\n",
60 resp.StatusCode,
64 if config.Openid.Authorize != "" {
65 openid_configuration.AuthorizationEndpoint = config.Openid.Authorize
68 jwks_json, err := io.ReadAll(resp.Body)
69 e(err)
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)
78 e(err)
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.
89 nonce := random(30)
90 return fmt.Sprintf(
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,
93 config.Openid.Client,
94 config.Url,
95 config.Openid.Redirect,
96 nonce,
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")
103 w.WriteHeader(405)
104 w.Write([]byte("Error: The OpenID Connect authorization endpoint only accepts POST requests.\n"))
105 return
108 err := req.ParseForm()
109 if err != nil {
110 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
111 w.WriteHeader(400)
112 w.Write([]byte("Error: Malformed form data.\n"))
113 return
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")
121 w.WriteHeader(400)
122 w.Write([]byte(fmt.Sprintf(
123 "Error: The OpenID Connect callback returned an error %s, but did not provide an error_description.\n",
124 returned_error,
126 return
127 } else {
128 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
129 w.WriteHeader(400)
130 w.Write([]byte(fmt.Sprintf(
131 "Error: The OpenID Connect callback returned an error:\n\n%s\n\n%s\n",
132 returned_error,
133 returned_error_description,
135 return
139 id_token_string := req.PostFormValue("id_token")
140 if id_token_string == "" {
141 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
142 w.WriteHeader(400)
143 w.Write([]byte(fmt.Sprintf("Error: The OpenID Connect callback did not return an error, but no id_token was found.\n")))
144 return
147 fmt.Println(id_token_string)
149 token, err := jwt.ParseWithClaims(
150 id_token_string,
151 &msclaims_t{},
152 openid_keyfunc.Keyfunc,
154 if err != nil {
155 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
156 w.WriteHeader(400)
157 w.Write([]byte(fmt.Sprintf("Error: Error parsing JWT with custom claims.\n")))
158 return
161 switch {
162 case token.Valid:
163 break
164 case errors.Is(err, jwt.ErrTokenMalformed):
165 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
166 w.WriteHeader(400)
167 w.Write([]byte(fmt.Sprintf("Error: Malformed JWT token.\n")))
168 return
169 case errors.Is(err, jwt.ErrTokenSignatureInvalid):
170 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
171 w.WriteHeader(400)
172 w.Write([]byte(fmt.Sprintf("Error: Invalid signature on JWT token.\n")))
173 return
174 case errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet):
175 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
176 w.WriteHeader(400)
177 w.Write([]byte(fmt.Sprintf("Error: JWT token expired or not yet valid.\n")))
178 return
179 default:
180 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
181 w.WriteHeader(400)
182 w.Write([]byte(fmt.Sprintf("Error: Funny JWT token.\n")))
183 return
186 claims, claims_ok := token.Claims.(*msclaims_t)
188 if !claims_ok {
189 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
190 w.WriteHeader(400)
191 w.Write([]byte(fmt.Sprintf("Error: JWT token's claims are not OK.\n")))
192 return
195 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
196 w.WriteHeader(200)
197 w.Write([]byte(fmt.Sprintf("Name: %s\nEmail: %s\nSubject: %s\n", claims.Name, claims.Email, claims.Subject)))
198 return