Skip to content

Commit

Permalink
Credentials list and revoking
Browse files Browse the repository at this point in the history
  • Loading branch information
kcze committed Sep 27, 2024
1 parent 230ec1c commit f930bf9
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def bearer(self) -> str:
class APIKeyCredentials(_BaseCredentials):
type: Literal["api_key"] = "api_key"
api_key: SecretStr
expires_at: Optional[int]
expires_at: Optional[int] = None
"""Unix timestamp (seconds) indicating when the API key expires (if at all)"""

def bearer(self) -> str:
Expand Down
5 changes: 5 additions & 0 deletions autogpt_platform/backend/backend/integrations/oauth/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ def exchange_code_for_tokens(self, code: str) -> OAuth2Credentials:
"""Exchanges the acquired authorization code from login for a set of tokens"""
...

@abstractmethod
def revoke_tokens(self, credentials: OAuth2Credentials) -> None:
"""Revokes the given tokens"""
...

@abstractmethod
def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
"""Implements the token refresh mechanism"""
Expand Down
18 changes: 18 additions & 0 deletions autogpt_platform/backend/backend/integrations/oauth/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class GitHubOAuthHandler(BaseOAuthHandler):

PROVIDER_NAME = "github"
EMAIL_ENDPOINT = "https://api.github.com/user/emails"
REVOKE_ENDPOINT = "https://api.github.com/applications/{client_id}/grant"

def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
Expand All @@ -44,6 +45,23 @@ def get_login_url(self, scopes: list[str], state: str) -> str:
def exchange_code_for_tokens(self, code: str) -> OAuth2Credentials:
return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri})

def revoke_tokens(self, credentials: OAuth2Credentials) -> None:
if not credentials.access_token:
raise ValueError("No access token to revoke")

headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}

response = requests.delete(
url=self.REVOKE_ENDPOINT.format(client_id=self.client_id),
auth=(self.client_id, self.client_secret),
headers=headers,
data={"access_token": credentials.access_token.get_secret_value()},
)
response.raise_for_status()

def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
if not credentials.refresh_token:
return credentials
Expand Down
10 changes: 10 additions & 0 deletions autogpt_platform/backend/backend/integrations/oauth/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class GoogleOAuthHandler(BaseOAuthHandler):

PROVIDER_NAME = "google"
EMAIL_ENDPOINT = "https://www.googleapis.com/oauth2/v2/userinfo"
REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke"

def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
Expand Down Expand Up @@ -59,6 +60,15 @@ def exchange_code_for_tokens(self, code: str) -> OAuth2Credentials:
scopes=google_creds.scopes,
)

def revoke_tokens(self, credentials: OAuth2Credentials) -> None:
session = AuthorizedSession(credentials)
response = session.post(
self.REVOKE_ENDPOINT,
params={"token": credentials.access_token.get_secret_value()},
headers={"content-type": "application/x-www-form-urlencoded"},
)
response.raise_for_status()

def _request_email(
self, creds: Credentials | ExternalAccountCredentials
) -> str | None:
Expand Down
4 changes: 4 additions & 0 deletions autogpt_platform/backend/backend/integrations/oauth/notion.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def exchange_code_for_tokens(self, code: str) -> OAuth2Credentials:
},
)

def revoke_tokens(self, credentials: OAuth2Credentials) -> None:
# Notion doesn't support token revocation
return

def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
# Notion doesn't support token refresh
return credentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ async def create_api_key_credentials(

@router.delete("/{provider}/credentials/{cred_id}", status_code=204)
async def delete_credential(
request: Request,
provider: Annotated[str, Path(title="The provider to delete credentials for")],
cred_id: Annotated[str, Path(title="The ID of the credentials to delete")],
user_id: Annotated[str, Depends(get_user_id)],
Expand All @@ -181,6 +182,10 @@ async def delete_credential(
status_code=404, detail="Credentials do not match the specified provider"
)

if isinstance(creds, OAuth2Credentials):
handler = _get_provider_oauth_handler(request, provider)
handler.revoke_tokens(creds)

store.delete_creds_by_id(user_id, cred_id)
return Response(status_code=204)

Expand Down
96 changes: 95 additions & 1 deletion autogpt_platform/frontend/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,46 @@ import { useSupabase } from "@/components/SupabaseProvider";
import { Button } from "@/components/ui/button";
import useUser from "@/hooks/useUser";
import { useRouter } from "next/navigation";
import { useCallback, useContext, useEffect, useMemo } from "react";
import { FaSpinner } from "react-icons/fa";
import {
CredentialsProviderData,
CredentialsProvidersContext,
} from "@/components/integrations/credentials-provider";
import { Separator } from "@/components/ui/separator";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { useToast } from "@/components/ui/use-toast";
import { Alert, AlertDescription } from "@/components/ui/alert";

export default function PrivatePage() {
const { user, isLoading, error } = useUser();
const { supabase } = useSupabase();
const router = useRouter();
const providers = useContext(CredentialsProvidersContext);
const api = useMemo(() => new AutoGPTServerAPI(), []);
const { toast } = useToast();

if (isLoading) {
const removeCredentials = useCallback(
async (provider: string, id: string) => {
try {
const response = await api.deleteCredentials(provider, id);
console.log("response", response);
toast({
title: "Credentials deleted",
duration: 2000,
});
} catch (error) {
toast({
title: "Something went wrong when deleting credentials " + error,
variant: "destructive",
duration: 2000,
});
}
},
[api],
);

if (isLoading || !providers || !providers) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
Expand All @@ -28,6 +60,68 @@ export default function PrivatePage() {
<div>
<p>Hello {user.email}</p>
<Button onClick={() => supabase.auth.signOut()}>Log out</Button>
<div>
<Alert className="mb-2 mt-2">
<AlertDescription>Heads up!</AlertDescription>
<AlertDescription>
<p>
You need to manually remove credentials from the Notion after
deleting them here, see{" "}
</p>
<a href="https://www.notion.so/help/add-and-manage-connections-with-the-api#manage-connections-in-your-workspace">
Notion documentation
</a>
</AlertDescription>
</Alert>
{Object.entries(providers).map(([providerName, provider]) => {
return (
<div key={provider.provider} className="mh-2">
<Separator />
<div className="text-xl">{provider.providerName}</div>
{provider.savedApiKeys.length > 0 && (
<div>
<div className="text-md">API Keys</div>
{provider.savedApiKeys.map((apiKey) => (
<div key={apiKey.id} className="flex flex-row">
<p className="p-2">
{apiKey.id} - {apiKey.title}
</p>
<Button
variant="destructive"
onClick={() =>
removeCredentials(providerName, apiKey.id)
}
>
Delete
</Button>
</div>
))}
</div>
)}
{provider.savedOAuthCredentials.length > 0 && (
<div>
<div className="text-md">OAuth Credentials</div>
{provider.savedOAuthCredentials.map((oauth) => (
<div key={oauth.id} className="flex flex-row">
<p className="p-2">
{oauth.id} - {oauth.title}
</p>
<Button
variant="destructive"
onClick={() =>
removeCredentials(providerName, oauth.id)
}
>
Delete
</Button>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div>
);
}

0 comments on commit f930bf9

Please sign in to comment.