Skip to content

Commit

Permalink
Merge pull request #56 from gremlin/oauth_completion
Browse files Browse the repository at this point in the history
Oauth completion
  • Loading branch information
khultman authored Jun 8, 2021
2 parents 79b13a9 + 96101e3 commit 8c97226
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 93 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.14.3
current_version = 0.15.1
commit = True
tag = True

Expand Down
30 changes: 14 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,33 +116,31 @@ config.team_id = team_id

#### Authentication with OAUTH

***Experimental - not fully implemented***

To authentication through a desired OAUTH workflow, the required information is similar to `gremlinapi.login()`.

When successfully authenticated through OAUTH, the bearer token, used later in the API workflow, is returned.

```python
from gremlinapi.oauth import GremlinAPIOAUTH

GREMLIN_COMPANY = "Hooli"
GREMLIN_USER = "[email protected]"
GREMLIN_PASSWORD = "y0urPa$$w0rd"
GREMLIN_COMPANY = "Your Company Name"
USERNAME = "[email protected]"
PASSWORD = "y0urPa$$w0rd"
OAUTH_LOGIN = "http://your.oauth.provider/login"

auth_args = {
"email":GREMLIN_USER,
"password": GREMLIN_PASSWORD,
"clientId": "mocklab_oauth2",
"companyName": GREMLIN_COMPANY,
"email":USERNAME,
"password": PASSWORD,
"client_id": "mocklab_oauth2",
"company_name": GREMLIN_COMPANY,
"oauth_login_uri": OAUTH_LOGIN,
}

bearer_token = GremlinAPIOAUTH.authenticate(**auth_args)
```

#### OAUTH Configuration

***Experimental - not fully implemented***

OAUTH can be configured through an API endpoint per the following configuration dictionary and code example.

You must previous be logged in or otherwise authenticated for the below code to succeed.
Expand All @@ -154,15 +152,15 @@ GREMLIN_TEAM_ID = "your-team-id"

config_body = {
# Used to authenticate against the OAuth provider. We will redirect the user to this URL when they initate a OAuth login.
"authorizationUri": "your-authorization-uri",
"authorizationUri": "http://your.oauth.provider/authorize",
# Used to exchange an OAuth code, obtained after logging into the OAuth provider, for an access token.
"tokenUri": "your-token-uri",
"tokenUri": "http://your.oauth.provider/oauth/token",
# Used to query for the email of the user..
"userInfoUri": "your-userinfo-uri",
"userInfoUri": "http://your.oauth.provider/userinfo",
# The public identifier obtained when registering Gremlin with your OAuth provider.
"clientId": "mocklab_oauth2",
"clientId": "your_client_id",
# The secret obtained when registering Gremlin with your OAuth provider.
"clientSecret": "foo",
"clientSecret": "your_client_secret",
# Define what level of access the access token will have that Gremlin obtains during the OAuth login. The default is `email`. If you change it from the default, the scope provided <strong>must</strong> be able to read the email of the user.
"scope":"email",
}
Expand Down
84 changes: 38 additions & 46 deletions gremlinapi/oauth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Kyle Bouchard <[email protected]>, Gremlin Inc <[email protected]>
# Copyright (C) 2021 Kyle Bouchard <[email protected]>, Gremlin Inc <[email protected]>

import logging
import json
Expand All @@ -26,13 +26,18 @@

from typing import Union, Type, Any, Tuple

from gremlinapi.util import (
GREMLIN_OAUTH_LOGIN,
GREMLIN_OAUTH_COMPANIES_URI,
GREMLIN_SSO_USER_AUTH,
GREMLIN_OAUTH_CALLBACK,
)

log = logging.getLogger("GremlinAPI.client")


class GremlinAPIOAUTH(GremlinAPI):
@classmethod
@experimental("`configure` method not fully implemented")
def configure(
cls,
company_id: str = "",
Expand All @@ -53,16 +58,16 @@ def configure(
`kwargs` is required in the following format:
{
companyId : Defines the Company ID for OAUTH
authorizationUri : Used to authenticate against the OAuth provider.
'companyId' : Defines the Company ID for OAUTH
'authorizationUri' : Used to authenticate against the OAuth provider.
We will redirect the user to this URL when they initate a OAuth login.
tokenUri : Used to exchange an OAuth code.
'tokenUri' : Used to exchange an OAuth code.
This is obtained after logging into the OAuth provider, for an access token.
userInfoUri : Used to query for the email of the user.
clientId : The public identifier obtained when registering Gremlin with your OAuth
'userInfoUri' : Used to query for the email of the user.
'clientId' : The public identifier obtained when registering Gremlin with your OAuth
provider.
clientSecret : The secret obtained when registering Gremlin with your OAuth provider.
scope : (OPTIONAL) Define what level of access the access token will have that Gremlin
'clientSecret' : The secret obtained when registering Gremlin with your OAuth provider.
'scope' : (OPTIONAL) Define what level of access the access token will have that Gremlin
obtains during the OAuth login. The default is `email`. If you change it from the
default, the scope provided must be able to read the email of the user.
}
Expand All @@ -78,9 +83,7 @@ def configure(
log.error(error_msg)
raise GremlinParameterError(error_msg)

endpoint: str = (
f"https://api.gremlin.com/v1/companies/{company_id}/oauth/settings"
)
endpoint: str = f"{GREMLIN_OAUTH_COMPANIES_URI}/{company_id}/oauth/settings"
data: dict = {
"authorizationUri": cls._error_if_not_param("authorizationUri", **kwargs),
"tokenUri": cls._error_if_not_param("tokenUri", **kwargs),
Expand All @@ -94,7 +97,6 @@ def configure(
return resp.status_code

@classmethod
@experimental
def initiate_oauth(
cls,
company_name: str,
Expand All @@ -113,12 +115,9 @@ def initiate_oauth(
state_cookie, oauth_provider_login_url : Tuple[str, str]
"""
endpoint: str = (
f"https://api.gremlin.com/v1/oauth/login?companyName={company_name}"
)
endpoint: str = f"{GREMLIN_OAUTH_LOGIN}?companyName={company_name}"

# Initiates OAUTH login with Gremlin
# `allow_redirects` = False enables capture of the response to extract the state cookie
# `status_code` 307 is a redirect to the OAUTH provider
payload: dict = cls._payload(**{"headers": https_client.header()})
(resp, body) = https_client.api_call("GET", endpoint, **payload)
Expand All @@ -132,7 +131,6 @@ def initiate_oauth(
return state_cookie, oauth_provider_login_url

@classmethod
@experimental
def get_callback_url(
cls,
oauth_provider_login_url: str,
Expand All @@ -151,37 +149,32 @@ def get_callback_url(
`data` is in the following format:
{
email: Login email for your user,
password: Login password for your user,
state: Value of the state cookie obtained in the previous step,
redirectUri: URL where your provider should redirect you to after authenticating.
'email': Login email for your user,
'password': Login password for your user,
'state': Value of the state cookie obtained in the previous step,
'redirectUri': URL where your provider should redirect you to after authenticating.
It should be https://api.gremlin.com/v1/oauth/callback,
clientId: Client Id obtained when registering Gremlin with your OAuth provider,
'clientId': Client Id obtained when registering Gremlin with your OAuth provider,
}
Returns
------
gremlin_callback_url : str
"""
payload: dict = cls._payload(**{"headers": https_client.header(), "body": data})
payload: dict = cls._payload(**{"headers": https_client.header(), "data": data})
(resp, body) = https_client.api_call(
"POST", oauth_provider_login_url, **payload
)

# You have now successfully authenticted with your OAuth provider,
# now continue the flow by following the redirect your OAuth provider
# created back to Gremlins /oauth/callback endpoint
# log.info(str(resp.headers))
gremlin_callback_url = resp.headers["Location"]
# gremlin_callback_url = auth_body["redirectUri"]
assert gremlin_callback_url != None
# NOTE TO SELF: the `location` key is nonexistant on the resp, and the
# redirectUri is `null` in the original state cookie as returned from the mock endpoint
return gremlin_callback_url

@classmethod
@experimental
def get_access_token(
cls,
state_cookie: str,
Expand Down Expand Up @@ -209,12 +202,9 @@ def get_access_token(
# redirect URL you are following and it needs to match the
# value in the cookie. This helps prevent CSRF attacks.
cookie = {"oauth_state": state_cookie}
payload: dict = cls._payload(
**{"headers": https_client.header(), "cookies": cookie}
(resp, body) = https_client.api_call(
"GET", gremlin_callback_url, **{"cookies": cookie}
)
(resp, body) = https_client.api_call("GET", gremlin_callback_url, **payload)

# gremlin_callback_response = requests.get(gremlin_callback_url, cookies=cookie)
# The response from the callback endpoint will contain the `access_token` in JSON
# This is the end of the OAuth specific flow. This `access_token` can
# now be exchanged for a Gremlin session.
Expand All @@ -225,7 +215,6 @@ def get_access_token(
return access_token

@classmethod
@experimental
def get_bearer_token(
cls,
company_name: str,
Expand All @@ -252,8 +241,8 @@ def get_bearer_token(
"accessToken": access_token,
"provider": "oauth",
}
payload: dict = cls._payload(**{"headers": https_client.header(), "body": body})
endpoint = f"https://api.gremlin.com/v1/users/auth/sso?getCompanySession=true"
payload: dict = cls._payload(**{"data": body})
endpoint = f"{GREMLIN_SSO_USER_AUTH}?getCompanySession=true"
(resp, body) = https_client.api_call("POST", endpoint, **payload)
assert resp.status_code == 200

Expand All @@ -267,7 +256,6 @@ def get_bearer_token(
return bearer_token

@classmethod
@experimental
def authenticate(
cls,
https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(),
Expand All @@ -284,29 +272,33 @@ def authenticate(
`kwargs` is required in the following format:
{
companyName : The company for which the oauth authentication should commence,
email: Login email for your user,
password: Login password for your user,
clientId: Client Id obtained when registering Gremlin with your OAuth provider,
'company_name' : The company for which the oauth authentication should commence,
'email': Login email for your user,
'password': Login password for your user,
'client_id': Client Id obtained when registering Gremlin with your OAuth provider,
'oauth_login_uri': The login Uri from your OAUTH provider,
}
Returns
------
bearer_token : str
"""
company_name = cls._error_if_not_param("companyName", **kwargs)

company_name = cls._error_if_not_param("company_name", **kwargs)
(state_cookie, oauth_provider_login_url) = cls.initiate_oauth(
company_name, https_client
)
auth_body = {
"email": cls._error_if_not_param("email", **kwargs),
"password": cls._error_if_not_param("password", **kwargs),
"state": state_cookie, # obtained in earlier step
"redirectUri": "https://api.gremlin.com/v1/oauth/callback",
"clientId": cls._error_if_not_param("clientId", **kwargs),
"redirectUri": GREMLIN_OAUTH_CALLBACK,
"clientId": cls._error_if_not_param("client_id", **kwargs),
}
gremlin_callback_url = cls.get_callback_url(oauth_provider_login_url, auth_body)
gremlin_callback_url = cls.get_callback_url(
cls._error_if_not_param("oauth_login_uri", **kwargs), auth_body
)
access_token = cls.get_access_token(state_cookie, gremlin_callback_url)
bearer_token = cls.get_bearer_token(company_name, access_token)
return bearer_token
8 changes: 6 additions & 2 deletions gremlinapi/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

log = logging.getLogger("GremlinAPI.client")


_version = "0.14.3"
_version = "0.15.1"


def get_version():
Expand All @@ -17,6 +16,11 @@ def get_version():

string_types = (type(b""), type(""), type(f""))

GREMLIN_OAUTH_LOGIN = "https://api.gremlin.com/v1/oauth/login"
GREMLIN_OAUTH_COMPANIES_URI = "https://api.gremlin.com/v1/companies"
GREMLIN_SSO_USER_AUTH = "https://api.gremlin.com/v1/users/auth/sso"
GREMLIN_OAUTH_CALLBACK = "https://api.gremlin.com/v1/oauth/callback"


def experimental(func):
"""
Expand Down
Loading

0 comments on commit 8c97226

Please sign in to comment.