Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bluesky OAuth Client #3473

Closed
wants to merge 66 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
cfd12f3
rm old login code
haileyok Apr 2, 2024
9128cb1
rm some more code and add some new code
haileyok Apr 2, 2024
103d441
remove some more unnecessary stuff
haileyok Apr 2, 2024
6856b76
add auth session browser for native
haileyok Apr 2, 2024
0c6eebf
remove unnecessary
haileyok Apr 2, 2024
fd8e886
a simple redirect on web
haileyok Apr 2, 2024
b4741a8
change
haileyok Apr 4, 2024
55acb43
merge main
haileyok Apr 4, 2024
872f5c0
merge main
haileyok Apr 4, 2024
c9c8e00
adjust `app.config.js` to prevent development manifest error
haileyok Apr 5, 2024
02697bb
rev change
haileyok Apr 5, 2024
b295e84
add oauth client
haileyok Apr 5, 2024
28acdf7
remove
haileyok Apr 5, 2024
aa32f84
Merge branch 'main' into hailey/oauth
haileyok Apr 5, 2024
857503e
Merge branch 'main' into hailey/oauth
haileyok Apr 8, 2024
d61fc5f
add `expo-secure-store`
haileyok Apr 8, 2024
71721b5
copy over a few files for now
haileyok Apr 8, 2024
352d375
save
haileyok Apr 8, 2024
db750e3
add `rn-quick-crypto` and `rn-quick-base64`
haileyok Apr 9, 2024
e165d49
metro config "polyfill"
haileyok Apr 9, 2024
ec58082
add jwk lib
haileyok Apr 9, 2024
1461c0a
decent base
haileyok Apr 9, 2024
c649795
remove all the test files
haileyok Apr 9, 2024
6522853
add `expo-sqlite`
haileyok Apr 10, 2024
ba4e95f
update deps (last time)
haileyok Apr 10, 2024
f7e8946
Merge branch 'main' into hailey/oauth
haileyok Apr 10, 2024
e831bc1
native crypto setup
haileyok Apr 10, 2024
25e5ee7
remove bogus packages
haileyok Apr 12, 2024
13607ed
squash
haileyok Apr 10, 2024
b521933
Merge branch 'hailey/expo-oauth-helper' into hailey/oauth
haileyok Apr 12, 2024
0aab04f
fix names
haileyok Apr 12, 2024
def2f77
Merge branch 'hailey/expo-oauth-helper' into hailey/oauth
haileyok Apr 12, 2024
72e22a2
revert babel changes
haileyok Apr 12, 2024
70251e5
few small changes
haileyok Apr 12, 2024
d948e73
update dev variables
haileyok Apr 12, 2024
73c98bc
native factory impl
haileyok Apr 12, 2024
00abca9
few more cleanups
haileyok Apr 12, 2024
472dbf5
Merge branch 'hailey/expo-oauth-helper' into hailey/oauth
haileyok Apr 12, 2024
951f5f2
it works!
haileyok Apr 12, 2024
bc67bdb
rm some useless code
haileyok Apr 12, 2024
c97e439
better layout
haileyok Apr 14, 2024
71f1e44
add jwt struct
haileyok Apr 15, 2024
2ada0cb
better implementation
haileyok Apr 15, 2024
2c1b370
swift impl
haileyok Apr 15, 2024
9f6db0e
few fixes
haileyok Apr 15, 2024
f4a2362
rm old files now that we have the skeleton ready
haileyok Apr 15, 2024
a07d291
add structs
haileyok Apr 15, 2024
adab484
oops
haileyok Apr 15, 2024
37f5840
update genkeypair
haileyok Apr 15, 2024
caddbeb
android impl
haileyok Apr 15, 2024
e01a127
rm log
haileyok Apr 15, 2024
de9f9ce
Merge branch 'hailey/expo-oauth-helper' into hailey/oauth
haileyok Apr 15, 2024
3e5a3ac
create factory (copy db for now)
haileyok Apr 15, 2024
bb4f973
Merge branch 'hailey/expo-oauth-helper' into hailey/oauth
haileyok Apr 15, 2024
db80f6e
changes
haileyok Apr 15, 2024
56f2baf
fix
haileyok Apr 15, 2024
dbbcd27
Merge branch 'hailey/expo-oauth-helper' into hailey/oauth
haileyok Apr 15, 2024
d56116a
few more things
haileyok Apr 15, 2024
c499a03
fix android
haileyok Apr 15, 2024
7ed21e4
rely on zod to remove os specific restraints
haileyok Apr 15, 2024
f464875
simplify kt
haileyok Apr 15, 2024
20f2988
more progress working through this
haileyok Apr 17, 2024
c7e2909
metro config temp
haileyok Apr 25, 2024
0d85013
feat(dev): allow using @atproto packages from another directory
matthieusieben Apr 23, 2024
77afff2
Merge branch 'dx-atproto-modules' into hailey/oauth
haileyok Apr 29, 2024
ea75b63
Merge branch 'hailey/oauth' into hailey/expo-oauth-helper
haileyok Apr 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions metro.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const path = require('path')
const {getDefaultConfig} = require('expo/metro-config')
const cfg = getDefaultConfig(__dirname)

if (process.env.ATPROTO_ROOT) {
const atprotoRoot = path.resolve(process.cwd(), process.env.ATPROTO_ROOT)

// Watch folders are used as roots for the virtual file system. Any file that
// needs to be resolved by the metro bundler must be within one of the watch
// folders. Since we will be resolving dependencies from the atproto packages,
// we need to add the atproto root to the watch folders so that the
cfg.watchFolders ||= []
cfg.watchFolders.push(atprotoRoot)

const resolveRequest = cfg.resolver.resolveRequest
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
// Alias @atproto/* modules to the corresponding package in the atproto root
if (moduleName.startsWith('@atproto/')) {
const [, packageName] = moduleName.split('/', 2)
const packagePath = path.join(atprotoRoot, 'packages', packageName)
return context.resolveRequest(context, packagePath, platform)
}

// Polyfills are added by the build process and are not actual dependencies
// of the @atproto/* packages. Resolve those from here.
if (
moduleName.startsWith('@babel/') &&
context.originModulePath.startsWith(atprotoRoot)
) {
return {
type: 'sourceFile',
filePath: require.resolve(moduleName),
}
}

return (resolveRequest || context.resolveRequest)(
context,
moduleName,
platform,
)
}
}

cfg.resolver.sourceExts = process.env.RN_SRC_EXT
? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
: cfg.resolver.sourceExts
Expand Down
93 changes: 93 additions & 0 deletions modules/expo-bluesky-oauth-client/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'

group = 'expo.modules.blueskyoauthclient'
version = '0.0.1'

buildscript {
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
if (expoModulesCorePlugin.exists()) {
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
}

// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

// Ensures backward compatibility
ext.getKotlinVersion = {
if (ext.has("kotlinVersion")) {
ext.kotlinVersion()
} else {
ext.safeExtGet("kotlinVersion", "1.8.10")
}
}

repositories {
mavenCentral()
}

dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
}
}

afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
}

android {
compileSdkVersion safeExtGet("compileSdkVersion", 33)

def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
}
}

namespace "expo.modules.blueskyoauthclient"
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1
versionName "0.0.1"
}
lintOptions {
abortOnError false
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}

repositories {
mavenCentral()
}

dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "com.nimbusds:nimbus-jose-jwt:9.38-rc3"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package expo.modules.blueskyoauthclient

import com.nimbusds.jose.Algorithm
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.interfaces.ECPublicKey
import java.security.interfaces.ECPrivateKey
import com.nimbusds.jose.jwk.Curve
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.KeyUse
import java.util.UUID

class CryptoUtil {
fun digest(data: ByteArray): ByteArray {
val digest = MessageDigest.getInstance("sha256")
return digest.digest(data)
}

fun getRandomValues(byteLength: Int): ByteArray {
val random = ByteArray(byteLength)
java.security.SecureRandom().nextBytes(random)
return random
}

fun generateKeyPair(): Map<String, String> {
val keyIdString = UUID.randomUUID().toString()

val keyPairGen = KeyPairGenerator.getInstance("EC")
keyPairGen.initialize(Curve.P_256.toECParameterSpec())
val keyPair = keyPairGen.generateKeyPair()

val publicKey = keyPair.public as ECPublicKey
val privateKey = keyPair.private as ECPrivateKey

val publicJwk = ECKey.Builder(Curve.P_256, publicKey)
.keyUse(KeyUse.SIGNATURE)
.algorithm(Algorithm.parse("ES256"))
.keyID(keyIdString)
.build()
val privateJwk = ECKey.Builder(Curve.P_256, publicKey)
.privateKey(privateKey)
.keyUse(KeyUse.SIGNATURE)
.keyID(keyIdString)
.algorithm(Algorithm.parse("ES256"))
.build()

return mapOf(
"privateKey" to privateJwk.toJSONString(),
"publicKey" to publicJwk.toJSONString()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package expo.modules.blueskyoauthclient

import android.util.Log
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class ExpoBlueskyOAuthClientModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoBlueskyOAuthClient")

AsyncFunction("digest") { value: ByteArray, algorithmName: String ->
if(algorithmName != "sha256") throw Exception("Unsupported algorithm")
return@AsyncFunction CryptoUtil().digest(value)
}

Function("getRandomValues") { byteLength: Int ->
return@Function CryptoUtil().getRandomValues(byteLength)
}

AsyncFunction("generateJwk") { algorithim: String? ->
if (algorithim != "ES256") {
throw Exception("Unsupported algorithm")
}
return@AsyncFunction CryptoUtil().generateKeyPair()
}

AsyncFunction("createJwt") { header: String, payload: String, jwk: String ->
return@AsyncFunction JWTUtil().createJwt(header, payload, jwk)
}

AsyncFunction("verifyJwt") { token: String, jwk: String ->
return@AsyncFunction JWTUtil().verifyJwt(token, jwk)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package expo.modules.blueskyoauthclient

import expo.modules.kotlin.records.Record
import expo.modules.kotlin.records.Field

class JWKPair(
@Field val privateKey: String,
@Field val publicKey: String
) : Record
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package expo.modules.blueskyoauthclient

import expo.modules.kotlin.records.Record
import expo.modules.kotlin.records.Field

class JWTVerifyResponse(
@Field var protectedHeader: String,
@Field var payload: String,
) : Record
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package expo.modules.blueskyoauthclient

import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.crypto.ECDSASigner
import com.nimbusds.jose.crypto.ECDSAVerifier
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT

class JWTUtil {
fun createJwt(header: String, payload: String, jwk: String): String {
val parsedKey = ECKey.parse(jwk)
val parsedHeader = JWSHeader.parse(header)
val parsedPayload = JWTClaimsSet.parse(payload)

val signer = ECDSASigner(parsedKey)
val jwt = SignedJWT(parsedHeader, parsedPayload)
jwt.sign(signer)

return jwt.serialize()
}

fun verifyJwt(token: String, jwk: String): Map<String, String> {
try {
val parsedKey = ECKey.parse(jwk)
val jwt = SignedJWT.parse(token)
val verifier = ECDSAVerifier(parsedKey)

if (!jwt.verify(verifier)) {
throw Exception("Invalid signature")
}

val header = jwt.header
val payload = jwt.payload

return mapOf(
"payload" to payload.toString(),
"protectedHeader" to header.toString()
)
} catch(e: Exception) {
throw e
}
}
}
9 changes: 9 additions & 0 deletions modules/expo-bluesky-oauth-client/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"platforms": ["ios", "tvos", "android", "web"],
"ios": {
"modules": ["ExpoBlueskyOAuthClientModule"]
},
"android": {
"modules": ["expo.modules.blueskyoauthclient.ExpoBlueskyOAuthClientModule"]
}
}
4 changes: 4 additions & 0 deletions modules/expo-bluesky-oauth-client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './src/oauth-client-react-native'
export * from './src/react-native-crypto-implementation'
export * from './src/react-native-key'
export * from './src/react-native-store-with-key'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, this is probably a good place to expose the "universal" module by having an index.web.ts that exposes the same interfaces/classes (but from @atproto/oauth-client-browser) ?

50 changes: 50 additions & 0 deletions modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import CryptoKit
import JOSESwift
import ExpoModulesCore

class CryptoUtil {
// The equivalent of crypto.subtle.digest() with JS on web
public static func digest(data: Data) -> Data {
let hash = SHA256.hash(data: data)
return Data(hash)
}

public static func getRandomValues(byteLength: Int) -> Data {
let bytes = (0..<byteLength).map { _ in UInt8.random(in: UInt8.min...UInt8.max) }
return Data(bytes)
}

public static func generateKeyPair() throws -> [String:String] {
let keyIdString = UUID().uuidString

let privateKey = P256.Signing.PrivateKey()
let publicKey = privateKey.publicKey

let x = publicKey.x963Representation[1..<33].base64URLEncodedString()
let y = publicKey.x963Representation[33...].base64URLEncodedString()
let d = privateKey.rawRepresentation.base64URLEncodedString()

let publicJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, alg: "ES256")
let privateJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, d: d, alg: "ES256")

return [
"privateKey": privateJWK.toJson(),
"publicKey": publicJWK.toJson()
]
}
}

extension Data {
func base64URLEncodedString() -> String {
return self.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
}
}

extension String {
func toField() -> Field<String> {
return Field(wrappedValue: self)
}
func toNullableField() -> Field<String?> {
return Field(wrappedValue: self)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Pod::Spec.new do |s|
s.name = 'ExpoBlueskyOAuthClient'
s.version = '0.0.1'
s.summary = 'A library of native functions to support Bluesky OAuth in React Native.'
s.description = 'A library of native functions to support Bluesky OAuth in React Native.'
s.author = ''
s.homepage = 'https://github.com/bluesky-social/social-app'
s.platforms = { :ios => '13.4', :tvos => '13.4' }
s.source = { git: '' }
s.static_framework = true

s.dependency 'ExpoModulesCore'
s.dependency 'JOSESwift', '~> 2.3'

# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
end
Loading
Loading