├── python-flask ├── .gitignore ├── requirements.txt ├── config.py ├── README.md └── run.py ├── node-express ├── package.json ├── config.js ├── README.md └── app.js ├── php ├── config.php ├── index.php ├── connected.php └── README.md ├── .gitignore ├── LICENSE └── README.md /python-flask/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv 3 | -------------------------------------------------------------------------------- /python-flask/requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2>=3.1.3 2 | MarkupSafe>=2.1.5 3 | Werkzeug>=3.0.1 4 | itsdangerous>=2.1.2 5 | requests>=2.31.0 6 | flask>=2.3.2 -------------------------------------------------------------------------------- /python-flask/config.py: -------------------------------------------------------------------------------- 1 | wpcc_consts = { 2 | "client_id": 1, # TODO 3 | "client_secret": "your-client-secret", # TODO 4 | "login_url": "http://localhost:5001/", # TODO 5 | "redirect_url": "http://localhost:5001/connected", # TODO 6 | "request_token_url": "https://public-api.wordpress.com/oauth2/token", 7 | "authenticate_url": "https://public-api.wordpress.com/oauth2/authenticate" 8 | } 9 | -------------------------------------------------------------------------------- /node-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress-express", 3 | "description": "WordPress.com Connect", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "dependencies": { 10 | "cookie-parser": "^1.4.7", 11 | "dotenv": "^16.5.0", 12 | "express": "^5.1.0", 13 | "express-session": "^1.18.1", 14 | "request": "^2.88.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /node-express/config.js: -------------------------------------------------------------------------------- 1 | // Replace the TODO values with the values shown on your app's Manage Settings page: https://developer.wordpress.com/apps/ 2 | // For more details, see the https://developer.wordpress.com/docs/oauth2/ 3 | 4 | module.exports = { 5 | client_id: 1, //TODO 6 | client_secret: "your-client-secret", //TODO 7 | login_url: "http://localhost:3000/", //TODO 8 | redirect_url: "http://localhost:3000/connected", //TODO 9 | request_token_url: "https://public-api.wordpress.com/oauth2/token", 10 | authenticate_url: "https://public-api.wordpress.com/oauth2/authenticate", 11 | }; 12 | -------------------------------------------------------------------------------- /php/config.php: -------------------------------------------------------------------------------- 1 | 'code', 16 | 'client_id' => CLIENT_ID, 17 | 'state' => $_SESSION['wpcc_state'], 18 | 'redirect_uri' => REDIRECT_URL 19 | )); 20 | 21 | echo '
';
23 |
--------------------------------------------------------------------------------
/python-flask/README.md:
--------------------------------------------------------------------------------
1 | # WordPress.com Connect Example (Flask)
2 |
3 | This is an example Flask application demonstrating how to connect with WordPress.com using OAuth2.
4 |
5 | ## Setup & Run Instructions
6 |
7 | 1. **Clone this repository and navigate to the `flask` directory:**
8 | ```sh
9 | cd flask
10 | ```
11 |
12 | 2. **Create and activate a virtual environment:**
13 | ```sh
14 | python3 -m venv venv
15 | source venv/bin/activate
16 | ```
17 |
18 | 3. **Install dependencies:**
19 | ```sh
20 | pip install -r requirements.txt
21 | ```
22 |
23 | 4. **Configure your WordPress.com credentials:**
24 | - Edit `config.py` and set your `client_id` and `client_secret`.
25 |
26 | 5. **Run the Flask app:**
27 | ```sh
28 | python run.py
29 | ```
30 |
31 | 6. **Open your browser and visit:**
32 | [http://localhost:5001/](http://localhost:5001/)
33 |
34 | ---
35 |
36 | **Note:**
37 | - If you change dependencies, re-run the install command.
38 | - If you encounter import errors, ensure your virtual environment is activated and all dependencies are installed.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to ' . $secret->access_token . '
'
32 | )
33 |
34 | resp.set_cookie('wpcc_state', state)
35 | return resp
36 |
37 |
38 | @app.route("/connected")
39 | def connected():
40 | app.logger.info("Connected page accessed")
41 | code = request.args.get('code')
42 |
43 | if not code:
44 | return redirect(url_for('login'))
45 |
46 | state = request.args.get('state')
47 | if not state:
48 | app.logger.warning('State parameter missing in callback URL')
49 | return 'Warning! State variable missing after authentication'
50 |
51 | wpcc_state = request.cookies.get('wpcc_state')
52 | if not wpcc_state:
53 | app.logger.warning('wpcc_state cookie missing')
54 | return 'Warning! State cookie missing. Authentication attempt may have been compromised.'
55 |
56 | if state != wpcc_state:
57 | app.logger.warning(
58 | 'State mismatch: received %s, expected %s', state, wpcc_state)
59 | return 'Warning! State mismatch. Authentication attempt may have been compromised.'
60 |
61 | # Optionally, clear the state cookie after use
62 | payload = {
63 | "client_id": wpcc_consts['client_id'],
64 | "redirect_uri": wpcc_consts['redirect_url'],
65 | "client_secret": wpcc_consts['client_secret'],
66 | "code": code, # The code from the previous request
67 | "grant_type": 'authorization_code'
68 | }
69 | r = requests.post(wpcc_consts['request_token_url'], data=payload)
70 | if 200 == r.status_code:
71 | token_data = r.json()
72 | access_token = token_data.get('access_token', 'No token found')
73 | resp = make_response(
74 | f''
75 | f'Access Token: ' 77 | f'{access_token}
' 78 | f'This token can be used to request more info about the user to the endpoint: ' 79 | f'https://developer.wordpress.com/docs/api/1.1/get/me
' 80 | f'Connection successful!
' 81 | f'' 82 | ) 83 | resp.set_cookie('wpcc_state', '', expires=0) 84 | return resp 85 | 86 | return 'Error: ' + r.text 87 | 88 | 89 | if __name__ == "__main__": 90 | app.debug = True 91 | app.run(host='localhost', port=5001) 92 | -------------------------------------------------------------------------------- /node-express/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const request = require('request'); 4 | const cookieParser = require('cookie-parser'); 5 | const session = require('express-session'); 6 | const crypto = require('crypto'); 7 | const wpcc_consts = require('./config.js'); 8 | 9 | const app = express(); 10 | const session_secret = process.env.SESSION_SECRET || 'FixMeBuildBetterSecretThanThis'; 11 | 12 | app.use(cookieParser()); 13 | app.use( 14 | session({ 15 | secret: session_secret, 16 | resave: false, 17 | saveUninitialized: true, 18 | cookie: { httpOnly: true, secure: false }, // Set secure: true if using HTTPS 19 | }) 20 | ); 21 | 22 | // Helper to build query string 23 | function buildQuery(params) { 24 | return Object.entries(params) 25 | .map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) 26 | .join('&'); 27 | } 28 | 29 | app.get('/', (req, res) => { 30 | // Generate a cryptographically secure random state 31 | const state = crypto.randomBytes(16).toString('hex'); 32 | req.session.wpcc_state = state; 33 | 34 | const params = { 35 | response_type: 'code', 36 | client_id: wpcc_consts.client_id, 37 | state, 38 | redirect_uri: wpcc_consts.redirect_url, 39 | }; 40 | const wpcc_url = `${wpcc_consts.authenticate_url}?${buildQuery(params)}`; 41 | 42 | const body = ` 43 | 44 | 45 | 46 | 47 |
52 |
53 | `;
54 |
55 | res.status(200).send(body);
56 | });
57 |
58 | app.get('/connected', (req, res) => {
59 | const { code, state } = req.query;
60 | if (code) {
61 | if (!state) {
62 | res.status(400).send('Warning! State variable missing after authentication');
63 | return;
64 | }
65 | if (state !== req.session.wpcc_state) {
66 | res.status(400).send('Warning! State mismatch. Authentication attempt may have been compromised.');
67 | return;
68 | }
69 |
70 | const post_data = {
71 | form: {
72 | client_id: wpcc_consts.client_id,
73 | redirect_uri: wpcc_consts.redirect_url,
74 | client_secret: wpcc_consts.client_secret,
75 | code,
76 | grant_type: 'authorization_code',
77 | },
78 | };
79 |
80 | request.post(
81 | wpcc_consts.request_token_url,
82 | post_data,
83 | (error, response, body) => {
84 | if (!error && response.statusCode === 200) {
85 | // TODO: In a real app, store the returned token securely
86 | const secret = JSON.parse(body);
87 | const html = `Access Token: ${secret.access_token}