├── .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 | 
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 |
--------------------------------------------------------------------------------
/pkce.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------