junobuild / juno-js

JavaScript libraries for interfacing with Juno
MIT License
13 stars 1 forks source link

Custom authentication using dfinity/agent #164

Open CodingFu opened 1 week ago

CodingFu commented 1 week ago

I am trying to implement third-party authentication mechanism on front-end. I'm trying to log in telegram web app into juno (because both nfid and internet identity work poorly in this environment)

Setup:

I have profiles table in juno (read: Public, write: Managed)

And i construct my identity in the following way:


const customIdentity = Ed25519KeyIdentity.generate()

// this doesnt work because reads are public :(
const profile = await getDoc({
  collection: "profiles",
  key: "123",
  satellite: {
    identity: customIdentity,
    satelliteId: "n5isd-iaaaa-aaaal-adtra-cai",
  }
})

// this works because reads are public
const profile = await getDoc({
  collection: "profiles",
  key: "123",
  doc: {
    username: "CodingFu"
  },
  satellite: {
    identity: customIdentity,
    satelliteId: "n5isd-iaaaa-aaaal-adtra-cai",
  }
})

I understand for the fact that user does not exist on authentication tab.

How can I add user with custom provider there? Is there any way i can "fake" authentication in juno? I know that i can make my customIdentity a controller for the satellite, but I can't do it for all the users. Plus I want to maintain "owner" field.

Please help!

peterpeterparker commented 1 week ago

How can I add user with custom provider there?

This is not yet supported. Providing interfaces for generic custom authentication is tracked in #156.

Is there any way i can "fake" authentication in juno?

I'm not sure what's "fake." Protected features require authenticated users. These are saved within a system collection named #user. If you are looking to hack around, one thing you could try is handling the reading and setting of such entities yourself. You can replicate the getDoc and setDoc methods from https://github.com/junobuild/juno-js/blob/51ecef384e001eab3d9c4aa254eb6df0b1a3a9f8/packages/core/src/services/user.services.ts#L16

This requires being able to obtain or derive an identity on your own—i.e., an identity is a must. It is worth noting that this can also potentially become insecure quickly; therefore, precautions are advised.

I know that i can make my customIdentity ...

If you are not using the built-in supported authentication (II and NFID), you have indeed have to pass the satellite parameters with your identity for each call.

Let me know if you have any other questions.

CodingFu commented 1 week ago

This was very very helpful! I managed to implement authentication! Thank you very much.

peterpeterparker commented 1 week ago

Cool! Is your solution open source? Would you ming sharing it?

CodingFu commented 1 week ago

let customIdentity: Identity | null = null;

export function setAPICustomIdentity(identity: Identity) {
  customIdentity = identity;
}

export function getSatteliteOptions(): SatelliteOptions {
  let options: SatelliteOptions = {};

  if (customIdentity) {
    options = {
      identity: customIdentity,
      satelliteId: import.meta.env.VITE_JUNO_ID as string,
    };
  }

  return options;
}
import React, { useEffect } from "react";
import { initCloudStorage } from "@tma.js/sdk"
import Loading from "../LoadingSplashScreen";
import { Ed25519KeyIdentity, ECDSAKeyIdentity } from "@dfinity/identity"
import { useRecoilState } from "recoil";
import { authState, customIdentity as customIdentityAtom } from "@/atoms/auth";
import { getSatteliteOptions, setAPICustomIdentity } from "@/api";
import {  fetchProfile, profileState } from "@/atoms/profile";
import { User, UserData, getDoc, setDoc } from "@junobuild/core";
import { Profile } from "@/types/entities";
import { useLocation, useNavigate } from "react-router-dom";
import { postEvent } from '@tma.js/sdk';

export default function TelegramAuthProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const IDENITITY_KEY = "telegram_internet_identity";
  const [isInitialized, setIsInitialized] = React.useState(false);
  const [, setUser] = useRecoilState<User | null>(authState);
  const [, setProfile] = useRecoilState<Profile | null>(profileState);

  const location = useLocation();
  const navigate = useNavigate();

  useEffect(() => {

    async function authorize() {
      const cloudStorage = initCloudStorage();

      console.log("authorizing")
      let restoredIdentity = await cloudStorage.get(IDENITITY_KEY);

      let identity : Ed25519KeyIdentity;
      if (!restoredIdentity) {
        console.log("generating identity")
        identity = await Ed25519KeyIdentity.generate();
        await cloudStorage.set(IDENITITY_KEY, JSON.stringify(identity.toJSON()));
        console.log("idenitity saved", identity.toJSON())
        console.log("principal:", identity.getPrincipal().toString())
      } else {
        identity = Ed25519KeyIdentity.fromJSON(restoredIdentity);
        console.log("restored identity", identity)
        console.log("principal:", identity.getPrincipal().toString())
      }

      setAPICustomIdentity(identity);

      const user = await getDoc<UserData>({
        collection: `#user`,
        key: identity.getPrincipal().toText(),
        satellite: getSatteliteOptions()
      });

      if (!user) {
        const result = await setDoc<UserData>({
          collection: `#user`,
          doc: {
            key: identity.getPrincipal().toText(),
            data: {
              provider: "internet_identity",
            },
          },
          satellite: getSatteliteOptions()
        });
        setUser(result);
      } else {
        setUser(user);
      }
      setIsInitialized(true);
    }

    authorize();
  }, []);

  if (!isInitialized) {
    return <Loading />;
  }

  return <>{children}</>;
}

Then whenever I do getDoc or setDoc I do it like that:

await setDoc({
  collection: 'profiles',
  doc: {
    key: '123',
    data: { username: 'CodingFu' }
  },
  satellite: getSatteliteOptions()
})
CodingFu commented 1 week ago

Baically i generate an identity and store it in telegram cloudStorage

it's like localStorage but it's synced between telegram clients on laptops, iphones, etc.

I'm still not 100% sure on security, but it does the trick.

I plan to move it out to auth canister and will make auth canister give me delegated identity, but while we are in prototyping phase, this approach works for me :)

CodingFu commented 1 week ago

@peterpeterparker

typescript nags at me, but what if I set provider to telegram in

const result = await setDoc<UserData>({
          collection: `#user`,
          doc: {
            key: identity.getPrincipal().toText(),
            data: {
              // @ts-ignore
              provider: "telegram",
            },
          },
          satellite: getSatteliteOptions()
        });

will it break the system? will satellite allow this?

peterpeterparker commented 1 week ago

Thanks a lot for the share, insteresting approach.

I'm still not 100% sure on security, but it does the trick.

I don't know how cloudStorage works but, indeed that does not feel super secure. Is the access to cloudStorage securised or anyone that basically knows telegram_internet_identity can retrieve the identity you stored? If the latest, it would be quite a loop hole.

I also don't know how things are encoded in cloudStorage...

typescript nags at me, but what if I set provider to telegram in

It's because typescript accepts only internet identity or nfid (see here). That will be extended once we develop the feature https://github.com/junobuild/juno-js/issues/156.