Skip to content

Commit c16ed8b

Browse files
author
GVE Devnet Admin
committed
Baselined from internal Repository
last_commit:05893928151678d266b454967c7ca62d1f10e6ef
1 parent e68c9bc commit c16ed8b

File tree

14 files changed

+375
-161
lines changed

14 files changed

+375
-161
lines changed

Dockerfile

Lines changed: 0 additions & 7 deletions
This file was deleted.

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ preventing unauthorized access to sensitive information.
2323
### Backend
2424
#### Retrieve Duo Credentials
2525
1. Follow the instructions under 'First Steps' to get your Duo integration key, secret key and API hostname: https://duo.com/docs/authapi.
26-
2. Clone this repository with git clone [repository name]
26+
2. Clone this repository with git clone `git clone https://github.com/gve-sw/gve_devnet_duo_manual_push_auth.git`.
2727
3. Create and update .env (see below)
2828
4. Proceed to 'usage' section.
2929

@@ -36,32 +36,41 @@ open .env
3636
```
3737
Copy/Paste the .env variables and update accordingly:
3838
```script
39-
DUO_IKEY=YOUR_DUO_INTEGRATION_KEY
40-
DUO_SKEY=YOUR_DUO_SECRET_KEY
41-
DUO_API_URL=YOUR_API_HOSTNAME
4239
APP_NAME='Duo Manual Push Authentication'
4340
APP_VERSION=1.0
4441
LOGGER_LEVEL=DEBUG
42+
DUO_API_URL=YOUR_API_HOSTNAME
43+
DUO_IKEY=YOUR_DUO_INTEGRATION_KEY
44+
DUO_SKEY=YOUR_DUO_SECRET_KEY
45+
DUO_ADMIN_API_URL=YOUR_ADMIN_API_HOSTNAME
46+
DUO_ADMIN_IKEY=YOUR_DUO_ADMIN_INTEGRATION_KEY
47+
DUO_ADMIN_SKEY=YOUR_DUO_ADMIN_SECRET_KEY
4548
```
46-
* Note: APP_NAME and APP_VERSION are for auto generated FastAPI docs at uvicorn_running_url/docs (i.e. http://127.0.0.1:8000/docs )
49+
* Note: APP_NAME and APP_VERSION are for auto-generated FastAPI docs at uvicorn_running_url/docs (i.e. http://127.0.0.1:8000/docs )
4750

4851
## Usage
49-
### 1. Start Frontend
52+
### With Docker
53+
#### 1. To build and start the containers, run: ``` docker-compose up --build ```
54+
#### 2. Navigate to URL: ``` http://localhost:5173/ ```
55+
#### 3. To stop the containers and remove them along with their network, run: ``` docker-compose down ```
56+
57+
### Without Docker
58+
#### 1. Start Frontend
5059
Terminal 1:
5160
```script
5261
cd frontend
5362
npm install
5463
npm run dev
5564
```
5665

57-
### 2. Start Backend
66+
#### 2. Start Backend
5867
Terminal 2:
5968
```scipt
6069
cd backend
6170
uvicorn main:app --reload
6271
```
6372

64-
### 3. Navigate to Frontend URL:
73+
#### 3. Navigate to Frontend URL:
6574
```script
6675
http://localhost:5173/
6776
```

backend/Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Backend/Dockerfile
2+
# Use an official Python runtime as a parent image
3+
FROM python:3.11-slim-buster
4+
5+
# Set the working directory in the container
6+
WORKDIR /app
7+
8+
# Copy the current directory contents into the container at /app
9+
COPY . .
10+
11+
# Install any needed packages specified in requirements.txt
12+
RUN pip install --no-cache-dir -r requirements.txt
13+
14+
# Make port 8000 available to the world outside this container
15+
EXPOSE 8000
16+
17+
# Define environment variable
18+
# ENV NAME World
19+
20+
# Run app.py when the container launches
21+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/config/config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from pydantic import field_validator
1919
from pydantic_settings import BaseSettings
2020

21-
2221
# Dynamically locate the .env file and handle cases where it might not exist.
2322
try:
2423
env_path = pathlib.Path(__file__).parents[0] / '.env'
@@ -43,6 +42,9 @@ class Config(BaseSettings):
4342
DUO_IKEY: str
4443
DUO_SKEY: str
4544

45+
DUO_ADMIN_API_URL: str
46+
DUO_ADMIN_IKEY: str
47+
DUO_ADMIN_SKEY: str
4648

4749
@field_validator('DUO_API_URL', mode='before')
4850
def validate_duo_api_url(cls, v):
@@ -51,4 +53,5 @@ def validate_duo_api_url(cls, v):
5153
return v
5254

5355

54-
config = Config() # Create a single instance of Config()
56+
57+
config = Config() # Create a single instance of Config()

backend/duo_app.py

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,22 @@
2020
import requests
2121
from urllib.parse import urlparse
2222
from config.config import config
23-
24-
# Access environment variables
25-
IKEY = config.DUO_IKEY
26-
SKEY = config.DUO_SKEY
27-
API_URL = config.DUO_API_URL
23+
import time
24+
from pprint import pprint
2825

2926

3027
class DuoAuthenticator:
31-
def __init__(self, ikey, skey, api_url):
32-
self.ikey = ikey
33-
self.skey = skey
34-
self.api_url = api_url
35-
self.host = self.parse_hostname(api_url)
28+
def __init__(self):
29+
# Access environment variables
30+
# For both Auth and Admin Duo API
31+
self.auth_ikey = config.DUO_IKEY
32+
self.auth_skey = config.DUO_SKEY
33+
self.auth_api_url = config.DUO_API_URL
34+
self.auth_host = self.parse_hostname(self.auth_api_url)
35+
self.admin_ikey = config.DUO_ADMIN_IKEY
36+
self.admin_skey = config.DUO_ADMIN_SKEY
37+
self.admin_api_url = config.DUO_ADMIN_API_URL
38+
self.admin_host = self.parse_hostname(self.admin_api_url)
3639

3740
def parse_hostname(self, url):
3841
parsed_url = urlparse(url)
@@ -43,49 +46,109 @@ def parse_hostname(self, url):
4346
return host
4447

4548
def generate_headers(self, method, path, params):
49+
if path.startswith('/admin'):
50+
ikey = self.admin_ikey
51+
skey = self.admin_skey
52+
host = self.admin_host
53+
else:
54+
ikey = self.auth_ikey
55+
skey = self.auth_skey
56+
host = self.auth_host
4657
now = email.utils.formatdate()
47-
canon = [now, method.upper(), self.host.lower(), path]
58+
canon = [now, method.upper(), host.lower(), path]
4859
args = []
4960
for key in sorted(params.keys()):
5061
val = params[key].encode("utf-8")
5162
args.append(
5263
'%s=%s' % (urllib.parse.quote(key, '~'), urllib.parse.quote(val, '~')))
5364
canon.append('&'.join(args))
5465
canon = '\n'.join(canon)
55-
sig = hmac.new(bytes(self.skey, encoding='utf-8'),
66+
sig = hmac.new(bytes(skey, encoding='utf-8'),
5667
bytes(canon, encoding='utf-8'), hashlib.sha1)
57-
auth = '%s:%s' % (self.ikey, sig.hexdigest())
68+
auth = '%s:%s' % (ikey, sig.hexdigest())
5869
return ('&'.join(args), {
5970
'Date': now,
6071
'Authorization': 'Basic %s' % base64.b64encode(bytes(auth, encoding="utf-8")).decode(),
6172
'Content-Type': 'application/x-www-form-urlencoded'
6273
})
6374

64-
def authenticate_user(self, user_email):
75+
def authenticate_user(self, payload):
76+
user_email = payload.get("email", "")
77+
username = payload.get("username", "")
78+
device = "auto" # or extract from payload's devices array if needed
79+
6580
uri = '/auth/v2/auth'
66-
params = {'username': user_email, 'factor': 'push', 'device': 'auto', 'async': '1'}
81+
params = {'username': username, 'factor': 'push', 'device': device, 'async': '1'}
6782
body, headers = self.generate_headers('POST', uri, params)
68-
response = requests.post(f'https://{self.host}{uri}', headers=headers, data=body).json()
83+
84+
# Use self.auth_host instead of self.host
85+
response = requests.post(f'https://{self.auth_host}{uri}', headers=headers, data=body).json()
86+
6987
if response['stat'] != 'OK':
7088
return response
7189
return self.check_auth_status(response['response']['txid'])
7290

7391
def check_auth_status(self, txid, timeout=60, interval=5):
74-
import time
7592
uri = '/auth/v2/auth_status'
7693
params = {'txid': txid}
7794
start_time = time.time()
7895
while True:
7996
if time.time() - start_time > timeout:
8097
return "Error: Timeout"
8198
args, headers = self.generate_headers('GET', uri, params)
82-
result = requests.get(f'https://{self.host}{uri}?{args}', headers=headers).json()
99+
result = requests.get(f'https://{self.auth_host}{uri}?{args}', headers=headers).json()
83100
if result['stat'] != 'OK':
84101
return result
85102
if result['response']['result'] in ['allow', 'deny']:
86103
return result['response']['result']
87104
time.sleep(interval)
88105

106+
def fetch_users(self):
107+
uri = '/admin/v1/users'
108+
params_f = '?limit={}&offset={}'
109+
result = []
110+
more = True
111+
limit = '100'
112+
offset = '0'
113+
while more:
114+
params = params_f.format(limit, offset)
115+
body, headers = self.generate_headers('GET', uri, {'limit': limit, 'offset': offset})
116+
response = requests.get(f'https://{self.admin_host}{uri}{params}', headers=headers).json()
117+
if response['stat'] != 'OK':
118+
return response
119+
for user in response['response']:
120+
if user['status'] != 'active' and user['status'] != 'bypass':
121+
continue
122+
user_dict = {
123+
'username': user['username'],
124+
'fullname': user['realname'],
125+
'email': user['email'],
126+
'status': user['status'],
127+
}
128+
devices = []
129+
for phone in user['phones']:
130+
if phone['activated']:
131+
try:
132+
phone['capabilities'].remove('auto')
133+
except ValueError:
134+
pass
135+
devices.append({
136+
'id': phone['phone_id'],
137+
'type': 'phone',
138+
'capabilities': phone['capabilities'],
139+
'model': phone['model'],
140+
'number': phone['number']
141+
})
142+
user_dict['devices'] = devices
143+
result.append(user_dict)
144+
if response['metadata'].get('next_offset'):
145+
pprint(response['metadata'])
146+
offset = str(response['metadata'].get('next_offset'))
147+
else:
148+
pprint(response['metadata'])
149+
more = False
150+
return result
151+
89152

90153
# Instantiate the DuoAuthenticator
91-
duo_authenticator = DuoAuthenticator(IKEY, SKEY, API_URL)
154+
duo_authenticator = DuoAuthenticator()
File renamed without changes.

backend/routes.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,50 @@
1010
IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
1111
or implied.
1212
"""
13-
1413
from fastapi import APIRouter, Request, Depends, HTTPException
14+
from typing import List, Optional
1515
from pydantic import BaseModel
1616
from logrr import logger_manager
1717
from duo_app import duo_authenticator
1818

1919
router = APIRouter()
2020

2121

22-
class UserRequest(BaseModel):
23-
email: str
22+
class Device(BaseModel):
23+
id: str
24+
type: str
25+
capabilities: List[str]
26+
model: Optional[str] = None
27+
number: Optional[str] = None
28+
29+
30+
class User(BaseModel):
31+
username: str
32+
fullname: str
33+
email: Optional[str] = None
34+
status: str
35+
devices: List[Device]
2436

2537

26-
# Using duo_app.py
2738
@router.post("/authenticate/")
28-
def authenticate(user_request: UserRequest):
39+
async def authenticate(user_request: User):
40+
try:
41+
user_request_dict = user_request.model_dump() # Convert the user_request object to a dictionary
42+
result = duo_authenticator.authenticate_user(user_request_dict) # Pass the user_request_dict to the authenticate_user function
43+
# Simulate authentication result for demonstration purposes (optional)
44+
# result = "Authentication successful for user: " + user_request.username
45+
return {"output": result}
46+
except Exception as e:
47+
raise HTTPException(status_code=500, detail=str(e)) # Log and handle the error
48+
49+
50+
@router.get("/users/")
51+
def users():
2952
try:
30-
logger_manager.console.print('[orange1]Authenticating with Duo...[/orange1]')
31-
result = duo_authenticator.authenticate_user(user_request.email)
32-
# result = duo_authenticator.parse_hostname(config.DUO_API_URL)
33-
logger_manager.console.print(f'Result {result}')
53+
logger_manager.console.print('[orange1]Fetching users...[/orange1]')
54+
result = duo_authenticator.fetch_users()
3455
return {"output": result}
3556
except Exception as e:
3657
# Log and handle the error
3758
logger_manager.console.print(f"[red]Error: {e}[/red]")
38-
raise HTTPException(status_code=500, detail=str(e))
59+
raise HTTPException(status_code=500, detail=str(e))

docker-compose.yml

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1-
version: "3.5"
1+
version: "3.8"
22

33
services:
4-
Duo_Manual_Push_Auth:
5-
image: ghcr.io/gve-sw/gve_devnet_duo_manual_push_auth:latest
6-
container_name: gve_devnet_duo_manual_push_auth:container
7-
environment:
8-
- SOME_VAR=""
4+
frontend:
5+
build:
6+
context: ./frontend
7+
dockerfile: Dockerfile
8+
restart: always
99
ports:
10+
- "5173:5173"
11+
environment:
12+
- NODE_ENV=development
1013

11-
volumes:
12-
- config.yaml:/main/config.yaml
13-
restart: "always"
14+
backend:
15+
env_file:
16+
- ./backend/config/.env
17+
build:
18+
context: ./backend
19+
dockerfile: Dockerfile
20+
restart: always
21+
ports:
22+
- "8000:8000"

frontend/Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Frontend/Dockerfile
2+
# Use an official Node runtime as a parent image
3+
FROM node:latest
4+
5+
# Set the working directory in the container
6+
WORKDIR /usr/src/app
7+
8+
# Copy package.json and package-lock.json
9+
COPY package*.json ./
10+
11+
# Try a clean install
12+
RUN npm ci
13+
14+
# Bundle app source
15+
COPY . .
16+
17+
# Your app binds to port 5173, so use the EXPOSE instruction
18+
EXPOSE 5173
19+
20+
# Define environment variable
21+
ENV NODE_ENV development
22+
23+
# Run npm run dev when the container launches
24+
CMD ["npm", "run", "dev"]

0 commit comments

Comments
 (0)