Nostr OAuth actions
This commit is contained in:
parent
41f676fdfb
commit
fd13ff70e3
|
@ -1,14 +1,31 @@
|
||||||
import { nip19 } from 'nostr-tools';
|
import { RootState, type AppDispatch } from 'soapbox/store';
|
||||||
|
|
||||||
import { type AppDispatch } from 'soapbox/store';
|
import { authLoggedIn, verifyCredentials } from './auth';
|
||||||
|
import { obtainOAuthToken } from './oauth';
|
||||||
|
|
||||||
import { verifyCredentials } from './auth';
|
const NOSTR_PUBKEY_SET = 'NOSTR_PUBKEY_SET';
|
||||||
|
|
||||||
/** Log in with a Nostr pubkey. */
|
/** Log in with a Nostr pubkey. */
|
||||||
function logInNostr(pubkey: string) {
|
function logInNostr(pubkey: string) {
|
||||||
return (dispatch: AppDispatch) => {
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const npub = nip19.npubEncode(pubkey);
|
dispatch(setNostrPubkey(pubkey));
|
||||||
return dispatch(verifyCredentials(npub));
|
|
||||||
|
const secret = sessionStorage.getItem('soapbox:nip46:secret');
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error('No secret found in session storage');
|
||||||
|
}
|
||||||
|
|
||||||
|
const relay = getState().instance.nostr?.relay;
|
||||||
|
|
||||||
|
const token = await dispatch(obtainOAuthToken({
|
||||||
|
grant_type: 'nostr',
|
||||||
|
pubkey,
|
||||||
|
relays: relay ? [relay] : undefined,
|
||||||
|
secret,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { access_token } = dispatch(authLoggedIn(token));
|
||||||
|
return await dispatch(verifyCredentials(access_token as string));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,4 +40,11 @@ function nostrExtensionLogIn() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { logInNostr, nostrExtensionLogIn };
|
function setNostrPubkey(pubkey: string) {
|
||||||
|
return {
|
||||||
|
type: NOSTR_PUBKEY_SET,
|
||||||
|
pubkey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { logInNostr, nostrExtensionLogIn, setNostrPubkey, NOSTR_PUBKEY_SET };
|
|
@ -20,7 +20,7 @@ export const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST';
|
||||||
export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS';
|
export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS';
|
||||||
export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
|
export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
|
||||||
|
|
||||||
export const obtainOAuthToken = (params: Record<string, string | undefined>, baseURL?: string) =>
|
export const obtainOAuthToken = (params: Record<string, unknown>, baseURL?: string) =>
|
||||||
(dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
|
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
|
||||||
return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => {
|
return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => {
|
||||||
|
|
|
@ -3,11 +3,15 @@ import { useEffect, useState } from 'react';
|
||||||
import { useNostr } from 'soapbox/contexts/nostr-context';
|
import { useNostr } from 'soapbox/contexts/nostr-context';
|
||||||
import { NConnect } from 'soapbox/features/nostr/NConnect';
|
import { NConnect } from 'soapbox/features/nostr/NConnect';
|
||||||
|
|
||||||
|
const secretStorageKey = 'soapbox:nip46:secret';
|
||||||
|
|
||||||
|
sessionStorage.setItem(secretStorageKey, crypto.randomUUID());
|
||||||
|
|
||||||
function useSignerStream() {
|
function useSignerStream() {
|
||||||
const { relay, signer } = useNostr();
|
const { relay, signer } = useNostr();
|
||||||
const [pubkey, setPubkey] = useState<string | undefined>(undefined);
|
const [pubkey, setPubkey] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const storageKey = `soapbox:nostr:auth:${pubkey}`;
|
const authStorageKey = `soapbox:nostr:auth:${pubkey}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (signer) {
|
if (signer) {
|
||||||
|
@ -21,8 +25,12 @@ function useSignerStream() {
|
||||||
const connect = new NConnect({
|
const connect = new NConnect({
|
||||||
relay,
|
relay,
|
||||||
signer,
|
signer,
|
||||||
onAuthorize: (authorizedPubkey) => localStorage.setItem(storageKey, authorizedPubkey),
|
onAuthorize(authorizedPubkey) {
|
||||||
authorizedPubkey: localStorage.getItem(storageKey) ?? undefined,
|
localStorage.setItem(authStorageKey, authorizedPubkey);
|
||||||
|
sessionStorage.setItem(secretStorageKey, crypto.randomUUID());
|
||||||
|
},
|
||||||
|
authorizedPubkey: localStorage.getItem(authStorageKey) ?? undefined,
|
||||||
|
getSecret: () => sessionStorage.getItem(secretStorageKey)!,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { NRelay, NRelay1, NostrSigner } from '@nostrify/nostrify';
|
||||||
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { NKeys } from 'soapbox/features/nostr/keys';
|
import { NKeys } from 'soapbox/features/nostr/keys';
|
||||||
import { useOwnAccount } from 'soapbox/hooks';
|
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance';
|
import { useInstance } from 'soapbox/hooks/useInstance';
|
||||||
|
|
||||||
interface NostrContextType {
|
interface NostrContextType {
|
||||||
|
@ -23,7 +23,7 @@ export const NostrProvider: React.FC<NostrProviderProps> = ({ children }) => {
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
|
|
||||||
const url = instance.nostr?.relay;
|
const url = instance.nostr?.relay;
|
||||||
const accountPubkey = account?.nostr.pubkey;
|
const accountPubkey = useAppSelector((state) => account?.nostr.pubkey ?? state.meta.pubkey);
|
||||||
|
|
||||||
const signer = useMemo(
|
const signer = useMemo(
|
||||||
() => (accountPubkey ? NKeys.get(accountPubkey) : undefined) ?? window.nostr,
|
() => (accountPubkey ? NKeys.get(accountPubkey) : undefined) ?? window.nostr,
|
||||||
|
|
|
@ -5,6 +5,7 @@ interface NConnectOpts {
|
||||||
signer: NostrSigner;
|
signer: NostrSigner;
|
||||||
authorizedPubkey: string | undefined;
|
authorizedPubkey: string | undefined;
|
||||||
onAuthorize(pubkey: string): void;
|
onAuthorize(pubkey: string): void;
|
||||||
|
getSecret(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NConnect {
|
export class NConnect {
|
||||||
|
@ -13,8 +14,8 @@ export class NConnect {
|
||||||
private signer: NostrSigner;
|
private signer: NostrSigner;
|
||||||
private authorizedPubkey: string | undefined;
|
private authorizedPubkey: string | undefined;
|
||||||
private onAuthorize: (pubkey: string) => void;
|
private onAuthorize: (pubkey: string) => void;
|
||||||
|
private getSecret: () => string;
|
||||||
|
|
||||||
public secret = crypto.randomUUID();
|
|
||||||
private controller = new AbortController();
|
private controller = new AbortController();
|
||||||
|
|
||||||
constructor(opts: NConnectOpts) {
|
constructor(opts: NConnectOpts) {
|
||||||
|
@ -22,6 +23,7 @@ export class NConnect {
|
||||||
this.signer = opts.signer;
|
this.signer = opts.signer;
|
||||||
this.authorizedPubkey = opts.authorizedPubkey;
|
this.authorizedPubkey = opts.authorizedPubkey;
|
||||||
this.onAuthorize = opts.onAuthorize;
|
this.onAuthorize = opts.onAuthorize;
|
||||||
|
this.getSecret = opts.getSecret;
|
||||||
|
|
||||||
this.open();
|
this.open();
|
||||||
}
|
}
|
||||||
|
@ -120,8 +122,7 @@ export class NConnect {
|
||||||
private async handleConnect(pubkey: string, request: NostrConnectRequest & { method: 'connect' }) {
|
private async handleConnect(pubkey: string, request: NostrConnectRequest & { method: 'connect' }) {
|
||||||
const [remotePubkey, secret] = request.params;
|
const [remotePubkey, secret] = request.params;
|
||||||
|
|
||||||
if (secret === this.secret && remotePubkey === await this.signer.getPublicKey()) {
|
if (secret === this.getSecret() && remotePubkey === await this.signer.getPublicKey()) {
|
||||||
this.secret = crypto.randomUUID();
|
|
||||||
this.authorizedPubkey = pubkey;
|
this.authorizedPubkey = pubkey;
|
||||||
this.onAuthorize(pubkey);
|
this.onAuthorize(pubkey);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Record as ImmutableRecord } from 'immutable';
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import { fetchInstance } from 'soapbox/actions/instance';
|
import { fetchInstance } from 'soapbox/actions/instance';
|
||||||
|
import { NOSTR_PUBKEY_SET } from 'soapbox/actions/nostr';
|
||||||
import { SW_UPDATING } from 'soapbox/actions/sw';
|
import { SW_UPDATING } from 'soapbox/actions/sw';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
@ -10,6 +11,8 @@ const ReducerRecord = ImmutableRecord({
|
||||||
instance_fetch_failed: false,
|
instance_fetch_failed: false,
|
||||||
/** Whether the ServiceWorker is currently updating (and we should display a loading screen). */
|
/** Whether the ServiceWorker is currently updating (and we should display a loading screen). */
|
||||||
swUpdating: false,
|
swUpdating: false,
|
||||||
|
/** User's nostr pubkey. */
|
||||||
|
pubkey: undefined as string | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function meta(state = ReducerRecord(), action: AnyAction) {
|
export default function meta(state = ReducerRecord(), action: AnyAction) {
|
||||||
|
@ -21,6 +24,8 @@ export default function meta(state = ReducerRecord(), action: AnyAction) {
|
||||||
return state;
|
return state;
|
||||||
case SW_UPDATING:
|
case SW_UPDATING:
|
||||||
return state.set('swUpdating', action.isUpdating);
|
return state.set('swUpdating', action.isUpdating);
|
||||||
|
case NOSTR_PUBKEY_SET:
|
||||||
|
return state.set('pubkey', action.pubkey);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue