├── .gitignore ├── pkce.png ├── implicit.png ├── mmdc.css ├── implicit.mmd ├── package.json ├── pkce.mmd ├── pkce-cli ├── README.md ├── implicit.svg └── pkce.svg /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /pkce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oktadev/pkce-cli/HEAD/pkce.png -------------------------------------------------------------------------------- /implicit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oktadev/pkce-cli/HEAD/implicit.png -------------------------------------------------------------------------------- /mmdc.css: -------------------------------------------------------------------------------- 1 | text.actor { 2 | font-family: 'trebuchet ms', verdana, arial; 3 | font-size: 14px; 4 | } 5 | 6 | .messageText { 7 | font-family: 'trebuchet ms', verdana, arial; 8 | font-size: 13px; 9 | } 10 | 11 | .noteText { 12 | font-family: 'trebuchet ms', verdana, arial; 13 | font-size: 13px; 14 | } 15 | -------------------------------------------------------------------------------- /implicit.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant RO as Resource Owner 3 | participant CA as Client App (vue.js) 4 | participant AS as Authorization Server 5 | RO->>CA: 1. Access App 6 | CA->>RO: 2. Redirect 7 | RO->>AS: 3. Redirect to Login 8 | AS->>RO: 4. Returns Login Form 9 | RO->>AS: 5. Submits Credentials 10 | AS->>RO: 6. Redirect with Token 11 | RO->>CA: 7. Redirect to App with Token 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pkce-cli", 3 | "version": "0.0.1", 4 | "description": "OAuth2 Authorization Code Flow with PKCE from the command line", 5 | "main": "index.js", 6 | "dependencies": { 7 | "commander": "^2.19.0", 8 | "opn": "https://github.com/dogeared/opn.git", 9 | "request": "^2.88.0", 10 | "restify": "^7.2.2" 11 | }, 12 | "devDependencies": {}, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "Micah Silverman", 17 | "license": "ISC" 18 | } 19 | -------------------------------------------------------------------------------- /pkce.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant RO as Resource Owner 3 | participant CA as Client App (vue.js) 4 | participant AS as Authorization Server 5 | RO->>CA: 1. Access App 6 | note right of CA: create random (v)
$ = sha256(v) 7 | CA->>RO: 2. Redirect with $ 8 | RO->>AS: 3. Redirect to Login with $ 9 | note right of AS: store $ 10 | AS->>RO: 4. Returns Login Form 11 | RO->>AS: 5. Submits Credentials 12 | note right of AS: authenticate user 13 | AS->>RO: 6. Redirect with code (α) 14 | RO->>CA: 7. Redirect to App with code (α) 15 | CA->>AS: 8. Request Token 16 | note over CA,AS: request includes:
Client ID, (v), (α) 17 | note right of AS: validate:
- Client ID
- sha256(v) = $
- (α) 18 | AS->>CA: 9. Return Token 19 | -------------------------------------------------------------------------------- /pkce-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var crypto = require('crypto'); 4 | var restify = require('restify'); 5 | var request = require('request'); 6 | var program = require('commander'); 7 | var opn = require('opn'); 8 | 9 | // Setup 10 | 11 | program 12 | .option('-c, --client_id ', 'OIDC Client ID', '') 13 | .option('-o, --okta_org ', 'ex: https://micah.oktapreview.com', '') 14 | .option('-s, --scopes ', 'Space separated list of scopes', 'openid profile email') 15 | .option('-r, --redirect_uri ', 'redirect uri', 'http://localhost:8080/redirect') 16 | .parse(process.argv); 17 | 18 | if ( 19 | !program.client_id || !program.okta_org || 20 | !program.scopes || !program.redirect_uri 21 | ) { 22 | program.help(); 23 | process.exit(1); 24 | } 25 | 26 | const server = restify.createServer({ 27 | name: 'myapp', 28 | version: '1.0.0' 29 | }); 30 | 31 | server.use(restify.plugins.acceptParser(server.acceptable)); 32 | server.use(restify.plugins.queryParser()); 33 | server.use(restify.plugins.bodyParser()); 34 | server.listen(8080); 35 | 36 | server.get('/redirect', oktaRedirectHandler); 37 | 38 | // execute auth code flow with pkce 39 | var codeVerifier = uuid(); 40 | console.log('Created Code Verifier (v): ' + codeVerifier + '\n'); 41 | var codeChallenge = base64url( 42 | crypto.createHash('sha256').update(codeVerifier).digest('base64') 43 | ); 44 | console.log('Created Code Challenge ($): ' + codeChallenge + '\n'); 45 | var authorizeUrl = buildAuthorizeUrl(codeVerifier, codeChallenge); 46 | console.log('About to call Authorize URL: ' + authorizeUrl + '\n'); 47 | 48 | console.log('press any key to continue...'); 49 | keypress().then(() => { 50 | // Step 1: call authorize endpoint where user will authenticate to Okta 51 | opn(authorizeUrl); 52 | }); 53 | 54 | // Step 2: Okta redirects back to this app with an auth code 55 | async function oktaRedirectHandler(req, res, next) { 56 | var body = '

OAuth2 authorize complete. ' + 57 | 'You can close this tab.

'; 58 | res.writeHead(200, { 59 | 'Content-Length': Buffer.byteLength(body), 60 | 'Content-Type': 'text/html' 61 | }); 62 | res.write(body); 63 | res.end(); 64 | 65 | console.log('\nGot code (α): ' + req.query.code + '\n'); 66 | 67 | console.log('press any key to continue...'); 68 | await keypress(); 69 | 70 | var form = { 71 | grant_type: 'authorization_code', 72 | redirect_uri: program.redirect_uri, 73 | client_id: program.client_id, 74 | code: req.query.code, 75 | code_verifier: codeVerifier 76 | }; 77 | 78 | console.log('\nCalling /token endpoint with:'); 79 | console.log('client_id: ' + form.client_id); 80 | console.log('code_verifier (v): ' + form.code_verifier); 81 | console.log('code (α): ' + form.code + '\n'); 82 | 83 | console.log( 84 | 'Here is the complete form post that will be sent to the /token endpoint:' 85 | ); 86 | console.log(form); 87 | console.log(); 88 | 89 | console.log('press any key to continue...'); 90 | await keypress(); 91 | 92 | // Step 3: call token endpoint where Okta will exchange code for tokens 93 | request.post( 94 | { 95 | url: program.okta_org + '/oauth2/v1/token', 96 | form: form 97 | }, 98 | function (err, httpResponse, body) { 99 | var tokenResponse = JSON.parse(body); 100 | tokenResponseHandler(tokenResponse); 101 | return next(); 102 | } 103 | ); 104 | } 105 | 106 | async function tokenResponseHandler(tokenResponse) { 107 | console.log('\nGot token response:'); 108 | console.log(tokenResponse); 109 | console.log(); 110 | 111 | console.log('press any key to continue...'); 112 | await keypress(); 113 | 114 | console.log('\nCalling /userinfo endpoint with access token\n'); 115 | 116 | // Step 4: use the access_token to hit the /userinfo endpoint 117 | request.get( 118 | program.okta_org + '/oauth2/v1/userinfo', 119 | { auth: { bearer: tokenResponse.access_token } }, 120 | function (err, httpResponse, body) { 121 | console.log(JSON.parse(body)); 122 | process.exit(0); 123 | } 124 | ); 125 | } 126 | 127 | function uuid() { 128 | function s4() { 129 | return Math.floor((1 + Math.random()) * 0x10000) 130 | .toString(16) 131 | .substring(1); 132 | } 133 | return s4() + '_' + s4() + '_' + s4() + '_' + s4() + '_' + 134 | s4() + '_' + s4() + '_' + s4() + '_' + s4() + '_' + 135 | s4() + '_' + s4() + '_' + s4() 136 | } 137 | 138 | function base64url(str){ 139 | return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); 140 | } 141 | 142 | function buildAuthorizeUrl(codeVerifier, codeChallenge) { 143 | var authorizeUrl = program.okta_org + '/oauth2/v1/authorize?' + 144 | 'client_id=' + program.client_id + '&' + 145 | 'response_type=code&' + 146 | 'scope=' + program.scopes + '&' + 147 | 'redirect_uri=' + program.redirect_uri + '&' + 148 | 'state=' + uuid() + '&' + 149 | 'code_challenge_method=S256&' + 150 | 'code_challenge=' + codeChallenge; 151 | return authorizeUrl; 152 | } 153 | 154 | async function keypress() { 155 | process.stdin.setRawMode(true) 156 | return new Promise(resolve => process.stdin.once('data', () => { 157 | process.stdin.setRawMode(false) 158 | resolve() 159 | })) 160 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PKCE Command Line (SPA example coming soon) 2 | 3 | This tool demonstrates the Authorization Code Flow with PKCE 4 | 5 | It follows these steps: 6 | 7 | 1. Creates a random string called the `code verifier` 8 | 2. Hashes the `code verifier` creating a value called the `code challenge` 9 | 3. Builds an authorization URL which includes: 10 | a. Okta OIDC Client ID 11 | b. a list of request scopes 12 | c. a redirect uri 13 | d. a randomly generated state value 14 | e. the `code challenge` 15 | f. a response type set to `code` to indicate that we're using the authorization code flow 16 | 4. Launches a browser with the authorization URL, at which point you will authenticate 17 | 5. Receives the redirect from the authorization url, which includes a `code` 18 | 6. Calls the `token` endpoint with: 19 | a. grant type set to `authorization code` 20 | b. a redirect uri that must match the one used in the authorization step 21 | c. Okta OIDC Client ID 22 | d. the `code` 23 | e. the `code verifier` from earlier 24 | 7. Displays the tokens returned from the `token` endpoint 25 | 8. Uses the returned access token to call the `userinfo` endpoint 26 | 27 | ## Usage 28 | 29 | ``` 30 | Usage: pkce-cli [options] 31 | 32 | Options: 33 | -c, --client_id OIDC Client ID (default: "") 34 | -o, --okta_org ex: https://micah.oktapreview.com (default: "") 35 | -s, --scopes Space separated list of scopes (default: "") 36 | -r, --redirect_uri redirect uri (default: "") 37 | -h, --help output usage information 38 | ``` 39 | 40 | ## Run 41 | 42 | ``` 43 | npm install 44 | ./pkce-cli \ 45 | --client_id 0oahdifc72URh7rUV0h7 \ 46 | --okta_org https://micah.oktapreview.com \ 47 | --scopes "openid profile email" \ 48 | --redirect_uri http://localhost:8080/redirect 49 | ``` 50 | 51 | You'll get output like this: 52 | 53 | ``` 54 | Created Code Verifier (v): 0233_39e5_6b3d_70b6_087f_b675_cc62_b178_ce21_577f_d661 55 | 56 | Created Code Challenge ($): Y3LBgtM-gcL_gEw-TGt26uOqNtnBO2nWXEwm_GC5Oh4 57 | 58 | Calling Authorize URL: https://micah.oktapreview.com/oauth2/v1/authorize?client_id=0oahdifc72URh7rUV0h7&response_type=code&scope=openid profile email&redirect_uri=http://localhost:8080/redirect&state=f3a5_f3a7_051f_2f97_f147_272a_d074_86fb_7d08_8650_3d8b&code_challenge_method=S256&code_challenge=Y3LBgtM-gcL_gEw-TGt26uOqNtnBO2nWXEwm_GC5Oh4 59 | 60 | Got code (α): C3LZZVjIYkOsjh42XTpZ 61 | 62 | Calling /token endpoint with: 63 | client_id: 0oahdifc72URh7rUV0h7 64 | code_verifier (v): 0233_39e5_6b3d_70b6_087f_b675_cc62_b178_ce21_577f_d661 65 | code (α): C3LZZVjIYkOsjh42XTpZ 66 | 67 | Here is the form post that will be sent to the /token endpoint: 68 | { grant_type: 'authorization_code', 69 | redirect_uri: 'http://localhost:8080/redirect', 70 | client_id: '0oahdifc72URh7rUV0h7', 71 | code: 'C3LZZVjIYkOsjh42XTpZ', 72 | code_verifier: '0233_39e5_6b3d_70b6_087f_b675_cc62_b178_ce21_577f_d661' } 73 | 74 | Got token response: 75 | { access_token: 76 | 'eyJraWQiOiItVV92MHBJVGx5X0V3MTJfTzZuT1lWb081ZVBucm1Iek9wbkxfS2FHN0lzIiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULnloZ0k4WFRTVWhrQ0dRT28xSVQwSXVYd3pSc094Rm5HY2xDNmN2bkFEMlkiLCJpc3MiOiJodHRwczovL21pY2FoLm9rdGFwcmV2aWV3LmNvbSIsImF1ZCI6Imh0dHBzOi8vbWljYWgub2t0YXByZXZpZXcuY29tIiwic3ViIjoibWljYWhAYWZpdG5lcmQuY29tIiwiaWF0IjoxNTQyMzg5NjY2LCJleHAiOjE1NDIzOTMyNjYsImNpZCI6IjBvYWhkaWZjNzJVUmg3clVWMGg3IiwidWlkIjoiMDB1ZG8zYmFseE5od0tkU0wwaDciLCJzY3AiOlsicHJvZmlsZSIsIm9wZW5pZCIsImVtYWlsIl19.db85XwSTta0UpxySopx6A66kBWybJtxgYZSP0EGoiTFV1dHHhlGR563J3zaF94a6m8rSIM9g_O_HBskLYr7uaZVTIlVq0pgT9v8NQ2dvwl5f0br9dfYgBv-ftGaSUr5BGJYTgM3urvx0x7M5HME_Me3id7tFQydvvcgJFOn_2nY8f-usy1jT8aJtOuxcgYqWrOVJmJJVRnI6tB8T7LT1GmIR9pe9dxaJxubqYIkCO2UeUCpBoLJ3duTpSIAmOFyMH1gxdXLHD4xQaBm-AKfMvLPSvEi-pH1soCXGX1dQVuwgBAiF7sNJNb4WueXu92AcSzma3jtEh0ri5RCYxywAbA', 77 | token_type: 'Bearer', 78 | expires_in: 3600, 79 | scope: 'profile openid email', 80 | id_token: 81 | 'eyJraWQiOiJUcGwtaVowcHhtQWpZb05ISlVSLUtjWkdCMGdUTWFUYkd1clQwd19GMXgwIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIwMHVkbzNiYWx4Tmh3S2RTTDBoNyIsIm5hbWUiOiJNaWNhaCBTaWx2ZXJtYW4iLCJlbWFpbCI6Im1pY2FoQGFmaXRuZXJkLmNvbSIsInZlciI6MSwiaXNzIjoiaHR0cHM6Ly9taWNhaC5va3RhcHJldmlldy5jb20iLCJhdWQiOiIwb2FoZGlmYzcyVVJoN3JVVjBoNyIsImlhdCI6MTU0MjM4OTY2NiwiZXhwIjoxNTQyMzkzMjY2LCJqdGkiOiJJRC5fS09kSjZITnpzNkpESkZHUEk5dlpTOHNkMk1memdGUW55bEpoRFpxaW53IiwiYW1yIjpbInB3ZCJdLCJpZHAiOiIwMG85dmFza2sySnQyWEZWZzBoNyIsInByZWZlcnJlZF91c2VybmFtZSI6Im1pY2FoQGFmaXRuZXJkLmNvbSIsImF1dGhfdGltZSI6MTU0MjM4OTYxMSwiYXRfaGFzaCI6IkQ3eU93WldjbzRTbFhGXzJJNnJpN0EifQ.XL1yFsx5gHl4fvngvTwVJwncfCyb5YwClwFpGsrUKr0MFDWBZuNmPvfPOFDBVkHbYmqUi3bajSYij7buI0mxauTJ1ZeqKeepUwLuVyKq94qbyHFXgvSlGXBYSXHA4sfJswJVSdkaoCXenyXTJJbcPuzYq6wpGt9a8ri4dq1cQ70UnXdgMTfbCGW_9Q6Tzv1wZa-GEB5i6iAfktrETORjMyFsGIAFaRQY5wdmsIf6LT3uIjKU7y4mq-X6rTJyJlkjmGxZv1QP0kfKiTSsGqeWt-s1-XinEtfnkOlLALNNIAo2MfB8cT88ixPZvCSt7VAzD_eBs8n_HkMqLQot4bs_Tw' } 82 | 83 | Calling /userinfo endpoint with access token 84 | 85 | { sub: '00udo3balxNhwKdSL0h7', 86 | name: 'Micah Silverman', 87 | profile: 88 | 'https://www.facebook.com/app_scoped_user_id/10156159259014459/', 89 | locale: 'en-US', 90 | email: 'micah@afitnerd.com', 91 | preferred_username: 'micah@afitnerd.com', 92 | given_name: 'Micah', 93 | family_name: 'Silverman', 94 | zoneinfo: 'America/Los_Angeles', 95 | updated_at: 1541796005, 96 | email_verified: true } 97 | ``` 98 | 99 | ## Diagrams 100 | 101 | Here's an overview of the Authorization Code with PKCE flow: 102 | 103 | ![pkce](pkce.png) 104 | 105 | Note: This image was generated using [mermaid](https://mermaidjs.github.io/). The source is [here](pkce.mmd) 106 | 107 | You can edit and regenrate the image using this command: 108 | 109 | ``` 110 | mmdc -i pkce.mmd -o pkce.png -b transparent -C mmdc.css 111 | mmdc -i pkce.mmd -o pkce.svg -C mmdc.css 112 | ``` 113 | -------------------------------------------------------------------------------- /implicit.svg: -------------------------------------------------------------------------------- 1 | Resource OwnerClient App (vue.js)Authorization Server1. Access App2. Redirect3. Redirect to Login4. Returns Login Form5. Submits Credentials6. Redirect with Token7. Redirect to App with TokenResource OwnerClient App (vue.js)Authorization Server -------------------------------------------------------------------------------- /pkce.svg: -------------------------------------------------------------------------------- 1 | Resource OwnerClient App (vue.js)Authorization Server1. Access Appcreate random (v)$ = sha256(v)2. Redirect with $3. Redirect to Login with $store $4. Returns Login Form5. Submits Credentialsauthenticate user6. Redirect with code (α)7. Redirect to App with code (α)8. Request Tokenrequest includes:Client ID, (v), (α)validate:- Client ID- sha256(v) = $- (α)9. Return TokenResource OwnerClient App (vue.js)Authorization Server --------------------------------------------------------------------------------