Skip to content

Commit bae22cf

Browse files
committed
Add automatic clipboard detection for JWT decoding
When no text is selected, the extension now automatically checks the clipboard for a valid JWT token. If found, it decodes it immediately without prompting. If the clipboard doesn't contain a valid JWT, it falls back to showing the input box as before. Includes a new JWT validation function that verifies token structure, format, and header validity, plus 10 additional test cases for comprehensive coverage.
1 parent ccabc3e commit bae22cf

7 files changed

Lines changed: 117 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ All notable changes to the "jwt-decoder" extension will be documented in this fi
44

55
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
66

7-
## [1.2.0] 2026-01-09
8-
### Added
9-
- Comprehensive test suite with 11 tests covering:
7+
## [1.3.0] 2026-01-09
8+
### Added
9+
- Automatic clipboard detection for JWT tokens
10+
- When no text is selected, the extension automatically checks clipboard for valid JWT
11+
- Smart validation ensures only valid JWTs are auto-decoded
12+
- Seamless fallback to input box if clipboard doesn't contain a valid JWT
13+
- JWT validation function to verify token structure and format
14+
- Comprehensive test suite with 23 tests covering:
1015
- JWT token decoding functionality
16+
- JWT validation (valid/invalid tokens, edge cases)
1117
- Hover content management
1218
- Extension activation and command registration
1319
- Edge cases and error handling

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ If you call the extension cammand against a `selected JWT string` from an Untitl
2222

2323
![](images/demo-from-untitled-document.gif)
2424

25-
### From input box to message box
25+
### From Clipboard or Input Box (fallback)
2626

27-
From any document, fire the extension command without text selection. Paste your JWT, hit enter and your decoded token will appear in a message box at the right bottom corner.
27+
From any document, fire the extension command without text selection. The extension will attempt to decode the clipboard value if it's a valid JWT. Otherwise, you'll be prompted to enter your JWT manually. The decoded token will appear in a message box at the right bottom corner.
2828

2929
![](images/demo-from-input-box.gif)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "A simple JWT decoder extension for VS Code",
55
"icon": "images/icon.png",
66
"publisher": "jflbr",
7-
"version": "1.2.0",
7+
"version": "1.3.0",
88
"engines": {
99
"vscode": "^1.85.0"
1010
},

src/extension.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
import * as vscode from 'vscode';
3-
import { decodeToken } from './jwt';
3+
import { decodeToken, isValidJWT } from './jwt';
44
import { setHoverContent } from './jwt';
55

66

@@ -56,7 +56,14 @@ export function activate(context: vscode.ExtensionContext) {
5656
hoverProvider.setTokenPosition(textEditor.selection.start);
5757
}
5858
else {
59-
token = await vscode.window.showInputBox({ placeHolder: 'Paste your base64 encoded JWT here' });
59+
// Try to get JWT from clipboard first
60+
const clipboardContent = await vscode.env.clipboard.readText();
61+
if (clipboardContent && isValidJWT(clipboardContent)) {
62+
token = clipboardContent.trim();
63+
} else {
64+
// Fallback to input box if clipboard doesn't contain a valid JWT
65+
token = await vscode.window.showInputBox({ placeHolder: 'Paste your base64 encoded JWT here' });
66+
}
6067
}
6168
if (token === null){
6269
vscode.window.showWarningMessage("Base64 encoded JWT required");

src/jwt.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,47 @@ function base64Decode(base64Str: string){
55
return JSON.parse(Buffer.from(base64, 'base64').toString('binary'));
66
}
77

8+
export function isValidJWT(token: string): boolean {
9+
if (!token || typeof token !== 'string') {
10+
return false;
11+
}
12+
13+
// Remove common prefixes and whitespace
14+
token = token.trim().replace(/^Bearer\s+/i, '');
15+
16+
// JWT should have exactly 2 dots (3 parts: header.payload.signature)
17+
const parts = token.split('.');
18+
if (parts.length !== 3) {
19+
return false;
20+
}
21+
22+
// Each part should be a valid base64url string (non-empty)
23+
for (const part of parts) {
24+
if (!part || part.length === 0) {
25+
return false;
26+
}
27+
// Base64url uses A-Z, a-z, 0-9, -, _
28+
if (!/^[A-Za-z0-9_-]+$/.test(part)) {
29+
return false;
30+
}
31+
}
32+
33+
// Try to decode the header to verify it's actually a JWT
34+
try {
35+
const headerBase64 = parts[0].replace('-', '+').replace('_', '/');
36+
const header = JSON.parse(Buffer.from(headerBase64, 'base64').toString('binary'));
37+
38+
// A valid JWT header should have at least an 'alg' field
39+
if (!header || typeof header !== 'object' || !header.alg) {
40+
return false;
41+
}
42+
} catch (e) {
43+
return false;
44+
}
45+
46+
return true;
47+
}
48+
849
export function decodeToken(token: string = '') {
950
let parts = token.split('.');
1051
let headers = parts[0];

src/test/suite/extension.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as assert from 'assert';
22
import { before } from 'mocha';
33
import * as vscode from 'vscode';
4-
import { decodeToken, setHoverContent } from '../../jwt';
4+
import { decodeToken, setHoverContent, isValidJWT } from '../../jwt';
55

66
suite('JWT Decoder Extension Test Suite', () => {
77
before(() => {
@@ -119,6 +119,58 @@ suite('JWT Decoder Extension Test Suite', () => {
119119
});
120120
});
121121

122+
suite('isValidJWT', () => {
123+
test('Should validate a valid JWT token', () => {
124+
const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
125+
assert.strictEqual(isValidJWT(validToken), true);
126+
});
127+
128+
test('Should validate JWT with Bearer prefix', () => {
129+
const tokenWithBearer = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
130+
assert.strictEqual(isValidJWT(tokenWithBearer), true);
131+
});
132+
133+
test('Should validate JWT with whitespace', () => {
134+
const tokenWithWhitespace = ' eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ';
135+
assert.strictEqual(isValidJWT(tokenWithWhitespace), true);
136+
});
137+
138+
test('Should reject token with only 2 parts', () => {
139+
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ';
140+
assert.strictEqual(isValidJWT(invalidToken), false);
141+
});
142+
143+
test('Should reject token with more than 3 parts', () => {
144+
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c.extra';
145+
assert.strictEqual(isValidJWT(invalidToken), false);
146+
});
147+
148+
test('Should reject empty string', () => {
149+
assert.strictEqual(isValidJWT(''), false);
150+
});
151+
152+
test('Should reject null or undefined', () => {
153+
assert.strictEqual(isValidJWT(null as any), false);
154+
assert.strictEqual(isValidJWT(undefined as any), false);
155+
});
156+
157+
test('Should reject token with invalid characters', () => {
158+
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI@invalid!.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
159+
assert.strictEqual(isValidJWT(invalidToken), false);
160+
});
161+
162+
test('Should reject token with empty parts', () => {
163+
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
164+
assert.strictEqual(isValidJWT(invalidToken), false);
165+
});
166+
167+
test('Should reject non-JWT strings', () => {
168+
assert.strictEqual(isValidJWT('This is not a JWT'), false);
169+
assert.strictEqual(isValidJWT('random.text.here'), false);
170+
assert.strictEqual(isValidJWT('123.456.789'), false);
171+
});
172+
});
173+
122174
suite('Extension Activation', () => {
123175
test('Extension should be present', () => {
124176
assert.ok(vscode.extensions.getExtension('jflbr.jwt-decoder'));

0 commit comments

Comments
 (0)