Skip to content

Instantly share code, notes, and snippets.

@Craftplacer
Last active April 19, 2022 12:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Craftplacer/a609deb92f4df7e2c254fdcf8e586eef to your computer and use it in GitHub Desktop.
Save Craftplacer/a609deb92f4df7e2c254fdcf8e586eef to your computer and use it in GitHub Desktop.
Dart implementation of https://oauth.net/core/1.0a
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:kaiteki/model/http_method.dart';
final DateTime _epoch = DateTime(1970);
/// Returns the seconds since January 1st 1970, required for the timestamp
/// parameter.
int secondsSinceEpoch() {
return (DateTime.now().difference(_epoch)).inSeconds;
}
/// Generates a random string of characters, required for the nonce parameter.
String generateNonce([int length = 32]) {
final random = Random.secure();
const chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final buffer = StringBuffer();
for (var i = 0; i < length; i++) {
final j = random.nextInt(chars.length - 1);
buffer.write(chars[j]);
}
return buffer.toString();
}
/// Generates a signature with HMAC-SHA1.
String generateHmacSha256Signature(
HttpMethod method,
Uri url,
Map<String, String> parameters,
String secret,
) {
final encodedSecret = encode(secret);
final key = utf8.encode(encodedSecret + "&");
final hmac = Hmac(sha1, key);
final data = getSignatureString(method, url, parameters);
final digest = hmac.convert(utf8.encode(data));
return base64.encode(digest.bytes);
}
String getSignatureString(
HttpMethod method,
Uri url,
Map<String, String> parameters,
) {
final paramStr = parameters.entries //
.map((kv) => kv.key + "=" + encode(kv.value))
.join("&");
final normalizedUrl = normalizeUrl(url);
return [
method.toMethodString(),
encode(normalizedUrl),
encode(paramStr),
].join("&");
}
String encode(String value) {
final buffer = StringBuffer();
final regex = RegExp("[0-9A-Za-z-._~]");
for (var i = 0; i < value.length; i++) {
final char = value[i];
if (regex.hasMatch(char)) {
buffer.write(char);
} else {
for (final byte in utf8.encode(char)) {
final hex = byte.toRadixString(16).padLeft(2, '0').toUpperCase();
buffer
..write("%")
..write(hex);
}
}
}
return buffer.toString();
}
String normalizeUrl(Uri url) {
final scheme = url.scheme.toLowerCase();
final isRedundantPort = (scheme == "http" && url.port == 80) ||
(scheme == "https" && url.port == 443);
return Uri(
scheme: url.scheme,
host: url.host,
port: isRedundantPort ? null : url.port,
path: url.path,
).toString().toLowerCase();
}
/// Returns a complete authorization header for OAuth v1.
///
/// Automatically adds missing parameters like nonce, signature and timestamp.
String generateAuthorizationHeader(
HttpMethod method,
Uri url,
Map<String, String> parameters,
String secret,
) {
parameters.addAll({
"oauth_nonce": generateNonce(),
"oauth_version": "1.0",
"oauth_timestamp": secondsSinceEpoch().toString(),
"oauth_signature_method": "HMAC-SHA1",
});
parameters = _sortMap(parameters); // Sort properly before generating sig
parameters["oauth_signature"] = generateHmacSha256Signature(
method,
url,
parameters,
secret,
);
parameters = _sortMap(parameters); // Sort properly before generating sig
return getAuthorizationHeader(parameters);
}
String getAuthorizationHeader(Map<String, String> parameters) {
final buffer = StringBuffer("OAuth ")
..writeAll(
parameters.entries.map(
(kv) => kv.key + '="' + encode(kv.value) + '"',
),
", ",
);
return buffer.toString();
}
Map<T1, T2> _sortMap<T1 extends Comparable, T2>(Map<T1, T2> map) {
final sortedEntries = map.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
return Map<T1, T2>.fromEntries(sortedEntries);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment