Skip to main content

wasm_oidc_plugin/
error.rs

1// proxy-wasm
2use proxy_wasm::traits::HttpContext;
3
4// html
5use html_escape::encode_safe;
6
7// thiserror
8use thiserror::Error;
9
10// crate
11use crate::auth::OidcHttpContext;
12
13/// Error type for the plugin
14#[derive(Error, Debug)]
15#[allow(clippy::enum_variant_names)]
16pub enum PluginError {
17    // Parsing Errors
18    #[error("url is not valid: {0}")]
19    UrlError(#[from] url::ParseError),
20    #[error("error while parsing the configuration file: {0}")]
21    YamlError(#[from] serde_yaml::Error),
22    #[error("error while parsing from json: {0}")]
23    JsonError(#[from] serde_json::Error),
24    #[error("error parsing utf8: {0}")]
25    Utf8Error(#[from] std::string::FromUtf8Error),
26    #[error("error while parsing from base64: {0}")]
27    DecodeError(#[from] base64::DecodeError),
28
29    // Token validation errors
30    #[error("error while getting code from callback: {0}")]
31    CodeNotFoundInCallbackError(#[from] serde_urlencoded::de::Error),
32    #[error("the code is coming from an unknown provider: {0}")]
33    ProviderNotFoundError(String),
34    #[error("token response is not in the required format: {0}")]
35    TokenResponseFormatError(String),
36    #[error("token validation failed: {0}")]
37    TokenValidationError(#[from] jwt_simple::Error),
38    #[error("no key worked for validation")]
39    NoKeyError,
40
41    // HTTP errors
42    #[error("dispatch failed, maybe you forgot to add the upstream?")]
43    DispatchError,
44    #[error("token_id mismatch")]
45    TokenIdMismatchError,
46    #[error("no body in response")]
47    NoBodyError,
48
49    // Cookie errors
50    #[error("encryption or decryption failed: {0}")]
51    AesError(#[from] aes_gcm::Error),
52    #[error("token could not be stored in cookie: {0}")]
53    CookieStoreError(String),
54    #[error("cookie is not valid: {0}")]
55    CookieValidationError(String),
56    #[error("session cookie not found")]
57    SessionCookieNotFoundError,
58    #[error("nonce cookie not found")]
59    NonceCookieNotFoundError,
60    #[error("authorization state not found")]
61    AuthorizationStateNotFoundError,
62    #[error("state does not match")]
63    StateMismatchError,
64}
65
66impl OidcHttpContext {
67    pub fn show_error_page(
68        &self,
69        status_code: u32,
70        title: &str,
71        message: &str,
72        show_reset_button: bool,
73    ) {
74        let headers = vec![("cache-control", "no-cache"), ("content-type", "text/html")];
75        let title = encode_safe(title);
76        let message = encode_safe(message);
77        let request_id = encode_safe(&self.request_id);
78        let reset_button = if show_reset_button {
79            r#"<a class="reset-button" href="/_wasm-oidc-plugin/clear-cookies">Reset Cookies</a>"#
80        } else {
81            ""
82        };
83
84        self.send_http_response(
85            status_code,
86            headers,
87            Some(
88                format!(
89                    r#"
90                    <!DOCTYPE html>
91                    <html lang="en">
92                    <head>
93                        <meta charset="UTF-8">
94                        <meta name="viewport" content="width=device-width, initial-scale=1.0">
95                        <title>Error - {status_code}</title>
96                        <style>
97                            :root {{
98                                --bg-color: #f0f2f5;
99                                --text-color: #333;
100                                --card-bg: #ffffff;
101                                --card-border: #e9ecef;
102                                --toggle-bg: #e2e8f0;
103                                --toggle-border: #cbd5e1;
104                                --button-bg: #3498db;
105                                --button-text: #ffffff;
106                                --button-hover: #2980b9;
107                            }}
108                            .dark-mode {{
109                                --bg-color: #1a1a1a;
110                                --text-color: #ffffff;
111                                --card-bg: #2c2c2c;
112                                --card-border: #4a4a4a;
113                                --toggle-bg: #4a5568;
114                                --toggle-border: #2d3748;
115                                --button-bg: #2980b9;
116                                --button-text: #ffffff;
117                                --button-hover: #3498db;
118                            }}
119                            body {{
120                                font-family: Helvetica, sans-serif;
121                                display: flex;
122                                flex-direction: column;
123                                justify-content: center;
124                                align-items: center;
125                                min-height: 100vh;
126                                margin: 0;
127                                background-color: var(--bg-color);
128                                color: var(--text-color);
129                                transition: background-color 0.3s ease, color 0.3s ease;
130                            }}
131                            .error-container {{
132                                text-align: center;
133                                padding: 40px;
134                                background-color: var(--card-bg);
135                                border-radius: 12px;
136                                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
137                                max-width: 600px;
138                                width: 90%;
139                            }}
140                            h1 {{
141                                margin-bottom: 10px;
142                            }}
143                            h2 {{
144                                margin-bottom: 20px;
145                                color: #e74c3c;
146                            }}
147                            p {{
148                                margin-bottom: 10px;
149                            }}
150                            .request-id {{
151                                font-size: 0.8em;
152                                color: #888;
153                            }}
154                            .dark-mode-toggle {{
155                                position: fixed;
156                                top: 20px;
157                                right: 20px;
158                            }}
159                            .toggle-switch {{
160                                position: relative;
161                                display: inline-block;
162                                width: 60px;
163                                height: 34px;
164                            }}
165                            .toggle-switch input {{
166                                opacity: 0;
167                                width: 0;
168                                height: 0;
169                            }}
170                            .toggle-slider {{
171                                position: absolute;
172                                cursor: pointer;
173                                top: 0;
174                                left: 0;
175                                right: 0;
176                                bottom: 0;
177                                background-color: var(--toggle-bg);
178                                border: 2px solid var(--toggle-border);
179                                transition: .4s;
180                                border-radius: 34px;
181                            }}
182                            .toggle-slider:before {{
183                                position: absolute;
184                                content: "☀️";
185                                display: flex;
186                                align-items: center;
187                                justify-content: center;
188                                height: 26px;
189                                width: 26px;
190                                left: 4px;
191                                bottom: 2px;
192                                background-color: white;
193                                transition: .4s;
194                                border-radius: 50%;
195                            }}
196                            input:checked + .toggle-slider {{
197                                background-color: var(--toggle-bg);
198                            }}
199                            input:checked + .toggle-slider:before {{
200                                transform: translateX(26px);
201                                content: "🌙";
202                                background-color: #2c3e50;
203                            }}
204                            .reset-button {{
205                                display: inline-block;
206                                margin-top: 20px;
207                                padding: 10px 20px;
208                                background-color: var(--button-bg);
209                                color: var(--button-text);
210                                border: none;
211                                border-radius: 5px;
212                                cursor: pointer;
213                                font-size: 16px;
214                                text-decoration: none;
215                                transition: background-color 0.3s ease;
216                            }}
217                            .reset-button:hover {{
218                                background-color: var(--button-hover);
219                            }}
220                        </style>
221                    </head>
222                    <body>
223                        <div class="dark-mode-toggle">
224                            <label class="toggle-switch">
225                                <input type="checkbox" id="darkModeToggle">
226                                <span class="toggle-slider"></span>
227                            </label>
228                        </div>
229                        <div class="error-container">
230                            <h1>Error {status_code}</h1>
231                            <h2>{title}</h2>
232                            <p>{message}</p>
233                            <p class="request-id">Request-ID: {request_id}</p>
234                            {reset_button}
235                        </div>
236                        <script>
237                            const darkModeToggle = document.getElementById('darkModeToggle');
238                            const body = document.body;
239
240                            function getDarkModePreference() {{
241                                try {{
242                                    return localStorage.getItem('darkMode');
243                                }} catch (error) {{
244                                    console.warn('localStorage is not available', error);
245                                    return null;
246                                }}
247                            }}
248
249                            function setDarkModePreference(value) {{
250                                try {{
251                                    if (value === null) {{
252                                        localStorage.removeItem('darkMode');
253                                    }} else {{
254                                        localStorage.setItem('darkMode', value);
255                                    }}
256                                }} catch (error) {{
257                                    console.warn('localStorage is not available', error);
258                                }}
259                            }}
260
261                            darkModeToggle.addEventListener('change', () => {{
262                                body.classList.toggle('dark-mode');
263                            }});
264
265                            // Check for saved dark mode preference
266                            if (getDarkModePreference() === 'enabled') {{
267                                body.classList.add('dark-mode');
268                                darkModeToggle.checked = true;
269                            }}
270
271                            // Save dark mode preference
272                            darkModeToggle.addEventListener('change', () => {{
273                                if (body.classList.contains('dark-mode')) {{
274                                    setDarkModePreference('enabled');
275                                }} else {{
276                                    setDarkModePreference(null);
277                                }}
278                            }});
279                        </script>
280                    </body>
281                    </html>
282                    "#,
283                )
284                .as_bytes(),
285            ),
286        );
287    }
288}