diff --git a/CHANGELOG.md b/CHANGELOG.md index 687440b..02dcd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## 1.0.0 +## 0.1.0 -- Initial version, created by Stagehand +- First version with every based features diff --git a/README.md b/README.md index 2913721..beaca04 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,2 @@ -A library for Dart developers. - -Created from templates made available by Stagehand under a BSD-style -[license](https://github.com/dart-lang/stagehand/blob/master/LICENSE). - -## Usage - -A simple usage example: - -```dart -import 'package:jsonwebtoken/jsonwebtoken.dart'; - -main() { - var awesome = new Awesome(); -} -``` - -## Features and bugs - -Please file feature requests and bugs at the [issue tracker][tracker]. - -[tracker]: http://example.com/issues/replaceme +# JsonWebToken +[![pub package](https://img.shields.io/pub/v/jsonwebtoken.svg)](https://pub.dev/packages/jsonwebtoken) diff --git a/example/example.dart b/example/example.dart index e850aae..7c4682f 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,11 +1,37 @@ import 'package:jsonwebtoken/jsonwebtoken.dart'; main() { - final token = JWT( - payload: { - 'hello': 'world', - }, - ).sign(key: 'test'); + String token; - print(token); + /* Sign */ { + // Create a json web token + final jwt = JWT( + payload: { + 'id': 123, + 'server': { + 'id': '3e4fc296', + 'loc': 'euw-2', + } + }, + issuer: 'https://github.com/jonasroussel/jsonwebtoken', + ); + + // Sign it + token = jwt.sign('secret-key'); + + print('Signed token: $token\n'); + } + + /* Verify */ { + try { + // Verify a token + final jwt = JWT.verify(token, 'secret-key'); + + print('Payload: ${jwt.payload}'); + } on JWTExpiredError { + print('jwt expired'); + } on JWTError catch (ex) { + print(ex.message); // ex: invalid signature + } + } } diff --git a/lib/jsonwebtoken.dart b/lib/jsonwebtoken.dart index 9830f96..ef29fd4 100644 --- a/lib/jsonwebtoken.dart +++ b/lib/jsonwebtoken.dart @@ -1,3 +1,5 @@ library jsonwebtoken; -export 'src/jsonwebtoken.dart'; +export 'src/jwt.dart'; +export 'src/errors.dart'; +export 'src/algorithms.dart'; diff --git a/lib/src/algorithms.dart b/lib/src/algorithms.dart new file mode 100644 index 0000000..be653ba --- /dev/null +++ b/lib/src/algorithms.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:jsonwebtoken/jsonwebtoken.dart'; + +abstract class JWTAlgorithm { + static const HS256 = HS256Algorithm(); + + static JWTAlgorithm fromName(String name) { + switch (name) { + case 'HS256': + return JWTAlgorithm.HS256; + default: + throw JWTInvalidError('unknown algorithm'); + } + } + + const JWTAlgorithm(); + + String get name; + List sign(String key, List body); + bool verify(String key, List body, List signature); +} + +class HS256Algorithm extends JWTAlgorithm { + const HS256Algorithm(); + + @override + String get name => 'HS256'; + + @override + List sign(String key, List body) { + final hmac = Hmac(sha256, utf8.encode(key)); + return hmac.convert(body).bytes; + } + + @override + bool verify(String key, List body, List signature) { + final actual = sign(key, body); + + if (actual.length != signature.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] != signature[i]) return false; + } + + return true; + } +} diff --git a/lib/src/errors.dart b/lib/src/errors.dart new file mode 100644 index 0000000..f9203fe --- /dev/null +++ b/lib/src/errors.dart @@ -0,0 +1,13 @@ +class JWTError extends Error { + JWTError(this.message); + + final String message; +} + +class JWTInvalidError extends JWTError { + JWTInvalidError(String message) : super(message); +} + +class JWTExpiredError extends JWTError { + JWTExpiredError() : super('jwt expired'); +} diff --git a/lib/src/jsonwebtoken.dart b/lib/src/jsonwebtoken.dart deleted file mode 100644 index 0b7ab68..0000000 --- a/lib/src/jsonwebtoken.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; - -class JWT { - JWT({ - this.payload = const {}, - this.audience, - this.subject, - this.issuer, - }); - - static String secretKey = null; - - final _jsonToBase64Url = json.fuse(utf8.fuse(base64Url)); - - final Map payload; - final String audience; - final String subject; - final String issuer; - - String signedToken; - - String sign({ - String key, - Duration expiresIn = null, - bool noTimestamp = false, - }) { - if (key == null && JWT.secretKey != null) key = JWT.secretKey; - assert(key != null); - - final header = {'alg': 'HS256', 'typ': 'JWT'}; - final algorithm = HS256Algorithm(key); - - // Creation timestamp - if (!noTimestamp) - payload['iat'] = DateTime.now(); - else - payload.remove('iat'); - - // Expiration timestamp - if (expiresIn != null) - payload['exp'] = DateTime.now().add(expiresIn); - else - payload.remove('exp'); - - final body = _jsonToBase64Url.encode(header) + '.' + _jsonToBase64Url.encode(payload); - final signature = base64Url.encode(algorithm.sign(utf8.encode(body))); - - return (signedToken = (body + '.' + signature)); - } - - static JWT verify(String token) {} -} - -abstract class JWTAlgorithm { - const JWTAlgorithm(); - - String get name; - List sign(List body); - bool verify(List body, List signature); -} - -class HS256Algorithm extends JWTAlgorithm { - const HS256Algorithm(this.secretKey); - - final String secretKey; - - @override - String get name => 'HS256'; - - @override - List sign(List body) { - final hmac = Hmac(sha256, utf8.encode(secretKey)); - return hmac.convert(body).bytes; - } - - @override - bool verify(List body, List signature) { - final actual = sign(body); - - if (actual.length != signature.length) return false; - - for (var i = 0; i < actual.length; i++) { - if (actual[i] != signature[i]) return false; - } - - return true; - } -} diff --git a/lib/src/jwt.dart b/lib/src/jwt.dart new file mode 100644 index 0000000..f723957 --- /dev/null +++ b/lib/src/jwt.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:jsonwebtoken/jsonwebtoken.dart'; + +import './utils.dart'; + +class JWT { + static JWT verify(String token, String key) { + try { + final parts = token.split('.'); + + final header = Map.from(jsonBase64.decode(base64Padded(parts[0]))); + + if (header['typ'] != 'JWT') throw JWTInvalidError('not a jwt'); + + final algorithm = JWTAlgorithm.fromName(header['alg']); + + if (parts.length < 3) throw JWTInvalidError('jwt malformated'); + + final body = utf8.encode(parts[0] + '.' + parts[1]); + final signature = base64Url.decode(base64Padded(parts[2])); + + if (!algorithm.verify(key, body, signature)) { + throw JWTInvalidError('invalid signature'); + } + + final payload = Map.from(jsonBase64.decode(base64Padded(parts[1]))); + + if (payload.containsKey('exp')) { + final exp = DateTime.fromMillisecondsSinceEpoch(payload['exp'] * 1000); + if (exp.isBefore(DateTime.now())) { + throw JWTExpiredError(); + } + } + + return JWT( + payload: payload, + audience: payload.remove('aud'), + issuer: payload.remove('iss'), + subject: payload.remove('sub'), + ); + } on JWTError catch (ex) { + throw ex; + } catch (ex) { + throw JWTInvalidError('jwt invalid'); + } + } + + JWT({ + this.payload = const {}, + this.audience, + this.subject, + this.issuer, + }); + + final Map payload; + final String audience; + final String subject; + final String issuer; + + String sign( + String key, { + JWTAlgorithm algorithm = JWTAlgorithm.HS256, + Duration expiresIn = null, + bool noTimestamp = false, + }) { + final header = {'alg': algorithm.name, 'typ': 'JWT'}; + + if (!noTimestamp) payload['iat'] = secondsSinceEpoch(DateTime.now()); + if (expiresIn != null) payload['exp'] = secondsSinceEpoch(DateTime.now().add(expiresIn)); + if (audience != null) payload['aud'] = audience; + if (subject != null) payload['sub'] = subject; + if (issuer != null) payload['iss'] = issuer; + + final body = base64Unpadded(jsonBase64.encode(header)) + '.' + base64Unpadded(jsonBase64.encode(payload)); + final signature = base64Unpadded(base64Url.encode(algorithm.sign(key, utf8.encode(body)))); + + return body + '.' + signature; + } +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart new file mode 100644 index 0000000..f846145 --- /dev/null +++ b/lib/src/utils.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; + +final jsonBase64 = json.fuse(utf8.fuse(base64Url)); + +String base64Unpadded(String value) { + if (value.endsWith('==')) return value.substring(0, value.length - 2); + if (value.endsWith('=')) return value.substring(0, value.length - 1); + return value; +} + +String base64Padded(String value) { + final lenght = value.length; + + switch (lenght % 4) { + case 2: + return value.padRight(lenght + 2, '='); + case 3: + return value.padRight(lenght + 1, '='); + default: + return value; + } +} + +int secondsSinceEpoch(DateTime time) { + return time.millisecondsSinceEpoch ~/ 1000; +} diff --git a/pubspec.yaml b/pubspec.yaml index 5f6902e..718bd84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,8 @@ name: jsonwebtoken -description: A starting point for Dart libraries or applications. -version: 1.0.0 -homepage: https://www.example.com +description: A dart implementation of JSON Web Tokens. +version: 0.1.0 +repository: https://github.com/jonasroussel/jsonwebtoken +homepage: https://github.com/jonasroussel/jsonwebtoken#readme environment: sdk: '>=2.7.0 <3.0.0'