├── .eleventy.js ├── .eleventyignore ├── .gitignore ├── README.md ├── netlify.toml ├── netlify └── functions │ ├── auth-before.js │ ├── auth-callback.js │ ├── dynamic │ └── index.js │ └── util │ ├── auth.js │ └── providers.js ├── package.json └── src ├── _data └── layout.js ├── _includes ├── layout.njk └── logout.njk ├── index.njk └── secure.njk /.eleventy.js: -------------------------------------------------------------------------------- 1 | const { EleventyServerlessBundlerPlugin } = require("@11ty/eleventy"); 2 | 3 | module.exports = function(eleventyConfig) { 4 | eleventyConfig.addPlugin(EleventyServerlessBundlerPlugin, { 5 | name: "dynamic", 6 | functionsDir: "./netlify/functions/", 7 | }); 8 | 9 | return { 10 | dir: { 11 | input: "src", 12 | } 13 | } 14 | }; -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | netlify -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .cache 3 | node_modules 4 | _site 5 | package-lock.json 6 | .netlify 7 | netlify/functions/dynamic/** 8 | !netlify/functions/dynamic/index.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # demo-eleventy-serverless-oauth 2 | 3 | A demo project using OAuth to secure some of your Eleventy Serverless routes. 4 | 5 | * [Demo](https://demo-eleventy-serverless-oauth.netlify.app) 6 | * [Deploy your own to Netlify](https://app.netlify.com/start/deploy?repository=https://github.com/11ty/demo-eleventy-serverless-oauth) 7 | 8 | --- 9 | 10 | * [Blog post on zachleat.com](https://www.zachleat.com/web/eleventy-login/) 11 | * [Walk-through on YouTube](https://www.youtube.com/watch?v=At19o2Ox57Y) 12 | 13 | ## Run locally 14 | 15 | ``` 16 | npm install 17 | npm run dev 18 | ``` 19 | 20 | Navigate to http://localhost:8888 21 | 22 | The full login flow is supported on localhost, assuming the Redirect URI set in your OAuth Application is configured correctly. 23 | 24 | ## OAuth Application Providers 25 | 26 | This example includes Netlify, GitHub, and GitLab providers. If you only want a subset of these providers, just remove the Login buttons that you don’t want and don’t worry about the relevant environment variables for that provider. 27 | 28 | 1. Create one or more OAuth applications: 29 | * [Netlify OAuth](https://app.netlify.com/user/applications/new) 30 | * [GitHub OAuth](https://github.com/settings/applications/new) 31 | * [GitLab](https://gitlab.com/-/profile/applications) 32 | * [Slack](https://api.slack.com/apps) (Redirect URI must be specified in separate Oauth & Permissions section) 33 | * [LinkedIn](https://www.linkedin.com/developers/apps) (To enable this you _must_ 1. create a LinkedIn Company Page and 2. add the [_Sign In With LinkedIn_ product under the Products tab](https://stackoverflow.com/questions/53479131/unauthorized-scope-error-in-linkedin-oauth2-authentication)) 34 | 2. Add the appropriate environment variables to your `.env` file: 35 | * Netlify: `NETLIFY_OAUTH_CLIENT_ID` and `NETLIFY_OAUTH_CLIENT_SECRET` 36 | * GitHub: `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET` 37 | * GitLab: `GITLAB_OAUTH_CLIENT_ID` and `GITLAB_OAUTH_CLIENT_SECRET` 38 | * Slack: `SLACK_OAUTH_CLIENT_ID` and `SLACK_OAUTH_CLIENT_SECRET` 39 | * LinkedIn: `LINKEDIN_OAUTH_CLIENT_ID` and `LINKEDIN_OAUTH_CLIENT_SECRET` 40 | 41 | For Netlify deployment you'll need to add these environment variables in the Netlify web app by defining them in Settings -> Build & Deploy -> Environment. 42 | 43 | Tip: For applications that don’t let you define multiple redirect URIs, I like to set up two OAuth applications: one for production and one for local development. That way I don’t have to worry about juggling the different Redirect URIs in the provider’s web interface. e.g. this will need to be `http://localhost:8888/.netlify/functions/auth-callback` for local development. 44 | 45 | ## Add this to your Eleventy site 46 | 47 | You will need a: 48 | * static login form 49 | * a secure serverless template 50 | 51 | ### Static login form 52 | 53 | Does not have to be in a serverless template. Put it in a shared header on your site! 54 | 55 | ``` 56 |
64 | ``` 65 | 66 | `securePath` should contain the URL to the secured serverless template (see next section). 67 | 68 | ### Serverless Templates 69 | 70 | Serverless templates can be secured with the following front matter (this example is YAML): 71 | 72 | ``` 73 | --- 74 | permalink: 75 | dynamic: "/YOUR_PATH_HERE/" 76 | secure: 77 | unauthenticatedRedirect: "/" 78 | --- 79 | ``` 80 | 81 | The above will secure the path and redirect any unauthenticated requests to the URL of your choosing. 82 | 83 | You can also conditionally render content inside of an insecure serverless template. Just check for the `user` global (you can rename this in `netlify/functions/dynamic/index.js`). Here’s an example of that: 84 | 85 | ``` 86 | --- 87 | permalink: 88 | dynamic: "/YOUR_PATH_HERE/" 89 | --- 90 | {% if user %} 91 | You are logged in! 92 | 93 | {% else %} 94 | 95 | {% endif %} 96 | ``` 97 | 98 | You can see an example of this in [the `everything-serverless` branch](https://github.com/11ty/demo-eleventy-serverless-oauth/compare/everything-serverless). 99 | 100 | #### More detail 101 | 102 | You may need to familiarize yourself with [Eleventy Serverless templates](https://www.11ty.dev/docs/plugins/serverless/#usage). 103 | 104 | Relevant files: 105 | * `.eleventy.js` adds the Elventy Serverless bundler plugin for the `dynamic` permalink. 106 | * Eleventy Serverless `.gitignore` additions: `netlify/functions/dynamic/**` and 107 | `!netlify/functions/dynamic/index.js` 108 | * Copy to your project: 109 | * `netlify/functions/dynamic/index.js` 110 | * `netlify/functions/util/*` 111 | * `netlify/functions/auth-before.js` 112 | * `netlify/functions/auth-callback.js` 113 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "_site" 3 | command = "npm run build" 4 | 5 | [[redirects]] 6 | from = "/secure/" 7 | to = "/.netlify/functions/dynamic" 8 | status = 200 9 | force = true 10 | _generated_by_eleventy_serverless = "dynamic" 11 | 12 | [[redirects]] 13 | from = "/" 14 | to = "/secure/" 15 | status = 302 16 | force = true 17 | 18 | [redirects.conditions] 19 | Cookie = [ "_11ty_oauth_token" ] 20 | -------------------------------------------------------------------------------- /netlify/functions/auth-before.js: -------------------------------------------------------------------------------- 1 | const { OAuth, getCookie, generateCsrfToken } = require("./util/auth.js"); 2 | const providers = require('./util/providers.js'); 3 | 4 | /* Do initial auth redirect */ 5 | exports.handler = async (event, context) => { 6 | 7 | if (!event.queryStringParameters) { 8 | return { 9 | statusCode: 401, 10 | body: JSON.stringify({ 11 | error: 'No token found', 12 | }) 13 | } 14 | } 15 | 16 | const csrfToken = generateCsrfToken(); 17 | const provider = event.queryStringParameters.provider; 18 | 19 | let oauth = new OAuth(provider); 20 | let config = oauth.config; 21 | 22 | const redirectUrl = (new URL(event.queryStringParameters.securePath, config.secureHost)).toString(); 23 | 24 | /* Generate authorizationURI */ 25 | const authorizationURI = oauth.authorizationCode.authorizeURL({ 26 | redirect_uri: config.redirect_uri, 27 | /* Specify how your app needs to access the user’s account. */ 28 | scope: providers[provider].scope || '', 29 | /* State helps mitigate CSRF attacks & Restore the previous state of your app */ 30 | state: `url=${redirectUrl}&csrf=${csrfToken}&provider=${provider}`, 31 | }); 32 | 33 | // console.log( "[auth-start] SETTING COOKIE" ); 34 | 35 | /* Redirect user to authorizationURI */ 36 | return { 37 | statusCode: 302, 38 | headers: { 39 | 'Set-Cookie': getCookie("_11ty_oauth_csrf", csrfToken, 60*2), // 2 minutes 40 | Location: authorizationURI, 41 | 'Cache-Control': 'no-cache' // Disable caching of this response 42 | }, 43 | body: '' // return body for local dev 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /netlify/functions/auth-callback.js: -------------------------------------------------------------------------------- 1 | const cookie = require("cookie"); 2 | const querystring = require("querystring"); 3 | const { OAuth, tokens, getCookie } = require("./util/auth.js"); 4 | 5 | // Function to handle netlify auth callback 6 | exports.handler = async (event, context) => { 7 | // Exit early 8 | if (!event.queryStringParameters) { 9 | return { 10 | statusCode: 401, 11 | body: JSON.stringify({ 12 | error: 'Not authorized', 13 | }) 14 | } 15 | } 16 | 17 | // Grant the grant code 18 | const code = event.queryStringParameters.code; 19 | 20 | // state helps mitigate CSRF attacks & Restore the previous state of your app 21 | const state = querystring.parse(event.queryStringParameters.state) 22 | 23 | try { 24 | // console.log( "[auth-callback] Cookies", event.headers.cookie ); 25 | let cookies = cookie.parse(event.headers.cookie); 26 | if(cookies._11ty_oauth_csrf !== state.csrf) { 27 | throw new Error("Missing or invalid CSRF token."); 28 | } 29 | 30 | let oauth = new OAuth(state.provider); 31 | let config = oauth.config; 32 | 33 | // Take the grant code and exchange for an accessToken 34 | const accessToken = await oauth.authorizationCode.getToken({ 35 | code: code, 36 | redirect_uri: config.redirect_uri, 37 | client_id: config.clientId, 38 | client_secret: config.clientSecret 39 | }); 40 | 41 | const token = accessToken.token.access_token; 42 | // console.log( "[auth-callback]", { token } ); 43 | 44 | // The noop key here is to workaround Netlify keeping query params on redirects 45 | // https://answers.netlify.com/t/changes-to-redirects-with-query-string-parameters-are-coming/23436/11 46 | const URI = `${state.url}?noop`; 47 | // console.log( "[auth-callback]", { URI }); 48 | 49 | /* Redirect user to authorizationURI */ 50 | return { 51 | statusCode: 302, 52 | headers: { 53 | Location: URI, 54 | 'Cache-Control': 'no-cache' // Disable caching of this response 55 | }, 56 | multiValueHeaders: { 57 | 'Set-Cookie': [ 58 | // This cookie *must* be HttpOnly 59 | getCookie("_11ty_oauth_token", tokens.encode(token), oauth.config.sessionExpiration), 60 | getCookie("_11ty_oauth_provider", state.provider, oauth.config.sessionExpiration), 61 | getCookie("_11ty_oauth_csrf", "", -1), 62 | ] 63 | }, 64 | body: '' // return body for local dev 65 | } 66 | 67 | } catch (e) { 68 | console.log("[auth-callback]", 'Access Token Error', e.message) 69 | console.log("[auth-callback]", e) 70 | return { 71 | statusCode: e.statusCode || 500, 72 | body: JSON.stringify({ 73 | error: e.message, 74 | }) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /netlify/functions/dynamic/index.js: -------------------------------------------------------------------------------- 1 | const cookie = require("cookie"); 2 | const { EleventyServerless } = require("@11ty/eleventy"); 3 | const { OAuth, tokens, getCookie } = require("../util/auth.js"); 4 | 5 | // Explicit dependencies for the bundler from config file and global data. 6 | // The file is generated by the Eleventy Serverless Bundler Plugin. 7 | require("./eleventy-bundler-modules.js"); 8 | 9 | async function handler(event) { 10 | let authToken; 11 | let provider; 12 | if(event.headers && event.headers.cookie) { 13 | let cookies = cookie.parse(event.headers.cookie); 14 | if(cookies._11ty_oauth_token) { 15 | authToken = tokens.decode(cookies._11ty_oauth_token); 16 | } 17 | if(cookies._11ty_oauth_provider) { 18 | provider = cookies._11ty_oauth_provider; 19 | } 20 | } 21 | 22 | let user; 23 | let authError; 24 | try { 25 | let oauth = new OAuth(provider); 26 | user = await oauth.getUser(authToken); 27 | } catch(e) { 28 | authError = e; 29 | } 30 | 31 | let elev = new EleventyServerless("dynamic", { 32 | path: event.path, 33 | query: event.queryStringParameters, 34 | functionsDir: "./netlify/functions/", 35 | config: function(eleventyConfig) { 36 | if(user) { 37 | eleventyConfig.addGlobalData("user", user); 38 | } 39 | 40 | // Adds `secure` data to JSON output 41 | eleventyConfig.dataFilterSelectors.add("secure"); 42 | } 43 | }); 44 | 45 | try { 46 | let [ page ] = await elev.getOutput(); 47 | 48 | if("logout" in event.queryStringParameters) { 49 | let redirectTarget = page.url; // default redirect to self 50 | if(page.data.secure && page.data.secure.unauthenticatedRedirect) { 51 | redirectTarget = page.data.secure.unauthenticatedRedirect; 52 | } 53 | 54 | // console.log( "Logging out" ); 55 | return { 56 | statusCode: 302, 57 | headers: { 58 | Location: redirectTarget, 59 | 'Cache-Control': 'no-cache' // Disable caching of this response 60 | }, 61 | multiValueHeaders: { 62 | 'Set-Cookie': [ 63 | getCookie("_11ty_oauth_token", "", -1), 64 | getCookie("_11ty_oauth_provider", "", -1), 65 | ] 66 | }, 67 | body: '' 68 | }; 69 | } 70 | 71 | // Secure pages 72 | if(page.data.secure && authError) { 73 | console.log("[serverless fn]", event.path, authToken, authError ); 74 | 75 | // unauthenticated redirect 76 | return { 77 | statusCode: 302, 78 | headers: { 79 | Location: page.data.secure.unauthenticatedRedirect || "/", 80 | 'Cache-Control': 'no-cache' // Disable caching of this response 81 | }, 82 | body: '' 83 | }; 84 | } 85 | 86 | return { 87 | statusCode: 200, 88 | headers: { 89 | "Content-Type": "text/html; charset=UTF-8", 90 | }, 91 | body: page.content, 92 | }; 93 | } catch (error) { 94 | // Only console log for matching serverless paths 95 | // (otherwise you’ll see a bunch of BrowserSync 404s for non-dynamic URLs during --serve) 96 | if (elev.isServerlessUrl(event.path)) { 97 | console.log("Serverless Error:", error); 98 | } 99 | 100 | return { 101 | statusCode: error.httpStatusCode || 500, 102 | body: JSON.stringify( 103 | { 104 | error: error.message, 105 | }, 106 | null, 107 | 2 108 | ), 109 | }; 110 | } 111 | } 112 | 113 | exports.handler = handler; -------------------------------------------------------------------------------- /netlify/functions/util/auth.js: -------------------------------------------------------------------------------- 1 | const { AuthorizationCode } = require('simple-oauth2'); 2 | const cookie = require("cookie"); 3 | const fetch = require('node-fetch') 4 | 5 | // Warning: process.env.DEPLOY_PRIME_URL won’t work in a Netlify function here. 6 | const SITE_URL = process.env.URL || 'http://localhost:8888'; 7 | 8 | const providers = require('./providers.js'); 9 | 10 | class OAuth { 11 | constructor(provider) { 12 | this.provider = provider; 13 | 14 | let config = this.config; 15 | this.authorizationCode = new AuthorizationCode({ 16 | client: { 17 | id: config.clientId, 18 | secret: config.clientSecret 19 | }, 20 | auth: { 21 | tokenHost: config.tokenHost, 22 | tokenPath: config.tokenPath, 23 | authorizePath: config.authorizePath 24 | } 25 | }); 26 | } 27 | 28 | get config() { 29 | const cfg = { 30 | secureHost: SITE_URL, 31 | sessionExpiration: 60 * 60 * 8, // in seconds, this is 8 hours 32 | 33 | /* redirect_uri is the callback url after successful signin */ 34 | redirect_uri: `${SITE_URL}/.netlify/functions/auth-callback`, 35 | } 36 | 37 | if(this.provider === "netlify") { 38 | Object.assign(cfg, providers.netlify); 39 | } else if(this.provider === "github") { 40 | Object.assign(cfg, providers.github); 41 | } else if(this.provider === "gitlab") { 42 | Object.assign(cfg, providers.gitlab); 43 | } else if(this.provider === "slack") { 44 | Object.assign(cfg, providers.slack); 45 | } else if(this.provider === "linkedin") { 46 | Object.assign(cfg, providers.linkedin); 47 | } else { 48 | throw new Error("Invalid provider passed to OAuth. Currently only `netlify`, `github`, `gitlab`, `slack` or `linkedin` are supported.") 49 | } 50 | 51 | cfg.clientId = process.env[cfg.clientIdKey]; 52 | cfg.clientSecret = process.env[cfg.clientSecretKey]; 53 | 54 | if (!cfg.clientId || !cfg.clientSecret) { 55 | throw new Error(`MISSING REQUIRED ENV VARS. ${cfg.clientIdKey} and ${cfg.clientSecretKey} are required.`) 56 | } 57 | 58 | return cfg; 59 | } 60 | 61 | async getUser(token) { 62 | if(!token) { 63 | throw new Error("Missing authorization token."); 64 | } 65 | 66 | const response = await fetch(this.config.userApi, { 67 | method: 'GET', 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | Authorization: `Bearer ${token}` 71 | } 72 | }) 73 | 74 | console.log( "[auth] getUser response status", response.status ); 75 | if (response.status !== 200) { 76 | throw new Error(`Error ${await response.text()}`) 77 | } 78 | 79 | const data = await response.json() 80 | return data 81 | } 82 | } 83 | 84 | function getCookie(name, value, expiration) { 85 | let options = { 86 | httpOnly: true, 87 | secure: true, 88 | sameSite: "Lax", 89 | path: '/', 90 | maxAge: expiration, 91 | }; 92 | 93 | // no strict cookies on localhost for local dev 94 | if(SITE_URL.startsWith("http://localhost:8888")) { 95 | delete options.sameSite; 96 | } 97 | 98 | return cookie.serialize(name, value, options) 99 | } 100 | 101 | function generateCsrfToken() { 102 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 103 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8) // eslint-disable-line 104 | return v.toString(16) 105 | }) 106 | } 107 | 108 | module.exports = { 109 | OAuth, 110 | tokens: { 111 | encode: function(token) { 112 | return Buffer.from(token, "utf8").toString("base64"); 113 | }, 114 | decode: function(token) { 115 | return Buffer.from(token, "base64").toString("utf8"); 116 | } 117 | }, 118 | getCookie, 119 | generateCsrfToken, 120 | } 121 | -------------------------------------------------------------------------------- /netlify/functions/util/providers.js: -------------------------------------------------------------------------------- 1 | const netlify = { 2 | clientIdKey: "NETLIFY_OAUTH_CLIENT_ID", 3 | clientSecretKey: "NETLIFY_OAUTH_CLIENT_SECRET", 4 | 5 | /* OAuth API endpoints */ 6 | tokenHost: 'https://api.netlify.com', 7 | tokenPath: 'https://api.netlify.com/oauth/token', 8 | authorizePath: 'https://app.netlify.com/authorize', 9 | 10 | /* User API endpoint */ 11 | userApi: "https://api.netlify.com/api/v1/user/", 12 | }; 13 | 14 | // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 15 | const github = { 16 | clientIdKey: "GITHUB_OAUTH_CLIENT_ID", 17 | clientSecretKey: "GITHUB_OAUTH_CLIENT_SECRET", 18 | 19 | /* OAuth API endpoints */ 20 | tokenHost: "https://github.com/", 21 | tokenPath: "https://github.com/login/oauth/access_token", 22 | authorizePath: "https://github.com/login/oauth/authorize", 23 | 24 | /* User API endpoint */ 25 | userApi: "https://api.github.com/user", 26 | }; 27 | 28 | // https://docs.gitlab.com/ee/api/oauth2.html 29 | const gitlab = { 30 | clientIdKey: "GITLAB_OAUTH_CLIENT_ID", 31 | clientSecretKey: "GITLAB_OAUTH_CLIENT_SECRET", 32 | 33 | /* OAuth API endpoints */ 34 | tokenHost: "https://gitlab.com/", 35 | tokenPath: "https://gitlab.com/oauth/token", 36 | authorizePath: "https://gitlab.com/oauth/authorize", 37 | 38 | /* Scope of access to request */ 39 | scope: 'read_user', 40 | 41 | /* User API endpoint */ 42 | userApi: "https://gitlab.com/api/v4/user", 43 | 44 | } 45 | 46 | const slack = { 47 | clientIdKey: "SLACK_OAUTH_CLIENT_ID", 48 | clientSecretKey: "SLACK_OAUTH_CLIENT_SECRET", 49 | 50 | /* OAuth API endpoints */ 51 | tokenHost: "https://slack.com/", 52 | tokenPath: "https://slack.com/api/openid.connect.token", 53 | authorizePath: "https://slack.com/openid/connect/authorize", 54 | 55 | /* Scope of access to request */ 56 | scope: 'openid email profile', 57 | 58 | /* User API endpoint */ 59 | userApi: "https://slack.com/api/openid.connect.userInfo", 60 | } 61 | 62 | const linkedin = { 63 | clientIdKey: "LINKEDIN_OAUTH_CLIENT_ID", 64 | clientSecretKey: "LINKEDIN_OAUTH_CLIENT_SECRET", 65 | 66 | /* OAuth API endpoints */ 67 | tokenHost: "https://linkedin.com/", 68 | tokenPath: "https://www.linkedin.com/oauth/v2/accessToken", 69 | authorizePath: "https://www.linkedin.com/oauth/v2/authorization", 70 | 71 | /* Scope of access to request */ 72 | scope: 'r_liteprofile r_emailaddress', 73 | 74 | /* User API endpoint */ 75 | userApi: "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))", 76 | } 77 | 78 | module.exports = { 79 | netlify, 80 | github, 81 | gitlab, 82 | slack, 83 | linkedin 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-eleventy-serverless-oauth", 3 | "version": "1.0.0", 4 | "description": "A demo showing how to implement OAuth using Eleventy Serverless (on Netlify)", 5 | "scripts": { 6 | "build": "npx @11ty/eleventy", 7 | "start": "npx @11ty/eleventy --serve", 8 | "dev": "netlify dev" 9 | }, 10 | "keywords": [], 11 | "author": { 12 | "name": "Zach Leatherman", 13 | "email": "zachleatherman@gmail.com", 14 | "url": "https://zachleat.com/" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "netlify-cli": "^6.14.7" 19 | }, 20 | "dependencies": { 21 | "@11ty/eleventy": "1.0.0-beta.4", 22 | "cookie": "^0.4.1", 23 | "dotenv": "^10.0.0", 24 | "node-fetch": "^2.3.0", 25 | "simple-oauth2": "^4.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/_data/layout.js: -------------------------------------------------------------------------------- 1 | // sitewide default layout 2 | module.exports = "layout.njk"; -------------------------------------------------------------------------------- /src/_includes/layout.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |15 | {{ user | dump(2) }} 16 |--------------------------------------------------------------------------------