Skip to content

Commit

Permalink
Merge pull request #224 from GSA/jf/uaa-login
Browse files Browse the repository at this point in the history
Add local dev login functionality
  • Loading branch information
jfredrickson authored Jun 21, 2023
2 parents fee950e + 357f780 commit 0c029e3
Show file tree
Hide file tree
Showing 31 changed files with 646 additions and 40 deletions.
4 changes: 4 additions & 0 deletions .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ DB_URI="postgres://postgres:postgres@localhost:5432/postgres"
# is fine for local development. In production, this needs to be set to the
# live website's URL.
BASE_URL="https://training.smartpay.gsa.gov"

# These are configured via config.py, but you can override them here if needed.
AUTH_CLIENT_ID="test_client_id"
AUTH_AUTHORITY_URL="http://localhost:8080/uaa"
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ Before the first deployment, you need to run the bootstrap script, where `SPACE`
bin/cg-bootstrap-space.sh SPACE
```

You'll also have to set up an identity provider service so that app administrators can log in via cloud.gov UAA. For each space, where `FRONT_END_BASE_URL` is the base URL of the front end website that will be running on cloud.gov Pages:

```
bin/cg-create-identity-service.sh SPACE FRONT_END_BASE_URL
# Examples:
# bin/cg-create-identity-service.sh dev https://federalist-2e11f2c8-970f-44f5-acc8-b47ef6c741ad.sites.pages.cloud.gov/site/gsa/smartpay-training
# bin/cg-create-identity-service.sh prod https://training.smartpay.gsa.gov
```

You can monitor the services deployment status with `cf services`. It can take quite a while to fully provision everything. Once the services are ready, you can bootstrap the application:

```
Expand Down
39 changes: 39 additions & 0 deletions bin/cg-create-identity-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash

# Creates an OAuth provider service in cloud.gov

set -e

if [ -z "$2" ] ; then
echo "Usage: $0 SPACE FRONT_END_BASE_URL"
echo
echo "Example: $0 prod https://training.smartpay.gsa.gov"
exit 1
fi

org="gsa-smartpay"
app_name="smartpay-training"
space=$1
redirect_url=${2%/}/auth_callback
post_logout_url=${2%/}
service_instance_name="smartpay-training-oauth-client"
service_key_name="smartpay-training-oauth-key"

echo "Creating identity provider service in space: $space"
echo "Service instance name: ${service_instance_name}"
echo "Service key name: ${service_key_name}"
echo "Redirect URL: ${redirect_url}"
echo "Post-logout URL: ${post_logout_url}"
echo

cf target -o ${org} -s ${space}

# Create identity provider
cf create-service cloud-gov-identity-provider oauth-client ${service_instance_name}

# Create service key
cf create-service-key smartpay-training-oauth-client ${service_key_name} \
-c "{\"redirect_uri\": [\"${redirect_url}\", \"${post_logout_url}\"]}"

echo If needed, you can retrieve the client_id and client_secret with:
echo cf service-key smartpay-training-oauth-client ${service_key_name}
1 change: 1 addition & 0 deletions manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ applications:
- smartpay-training-db
- smartpay-training-redis
- smartpay-training-secrets
- smartpay-training-oauth-client
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
redis==4.4.4
fastapi==0.97.0
uvicorn[standard]==0.20.0
pyjwt==2.6.0
pyjwt[crypto]==2.6.0
fastapi_mail==1.2.5
cfenv==0.5.3
SQLAlchemy==2.0.5.post1
psycopg2==2.9.5
alembic==1.10.2
PyMuPDF==1.21.1
python-multipart==0.0.6
python-multipart==0.0.6
2 changes: 1 addition & 1 deletion training-front-end/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
PUBLIC_API_BASE_URL=http://localhost:8000
# in minutes
PUBLIC_SESSION_TIME_OUT=60
PUBLIC_SESSION_WARNING_TIME=5
PUBLIC_SESSION_WARNING_TIME=5
2 changes: 1 addition & 1 deletion training-front-end/.env.production
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PUBLIC_API_BASE_URL=https://smartpay-training-dev.app.cloud.gov
# in minutes
PUBLIC_SESSION_TIME_OUT=30
PUBLIC_SESSION_WARNING_TIME=5
PUBLIC_SESSION_WARNING_TIME=5
2 changes: 1 addition & 1 deletion training-front-end/.env.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PUBLIC_API_BASE_URL=http://localhost:8000
# in minutes
PUBLIC_SESSION_TIME_OUT=30
PUBLIC_SESSION_WARNING_TIME=5
PUBLIC_SESSION_WARNING_TIME=5
2 changes: 1 addition & 1 deletion training-front-end/.env.user_test
Original file line number Diff line number Diff line change
@@ -1 +1 @@
PUBLIC_API_BASE_URL=https://smartpay-training-test.app.cloud.gov
PUBLIC_API_BASE_URL=https://smartpay-training-test.app.cloud.gov
23 changes: 23 additions & 0 deletions training-front-end/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions training-front-end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"install": "^0.13.0",
"nanostores": "^0.8.0",
"npm": "^9.6.3",
"oidc-client-ts": "^2.2.4",
"remark-custom-heading-id": "^1.0.1",
"vue": "^3.2.47"
},
Expand Down
69 changes: 69 additions & 0 deletions training-front-end/src/components/AdminIndex.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup>
import { onBeforeMount, ref } from 'vue'
import { useStore } from '@nanostores/vue'
import { profile } from '../stores/user'
import USWDSAlert from './USWDSAlert.vue'
const authUser = useStore(profile)
const userList = ref([])
const error = ref()
const isAuthorized = ref(true)
function loadUsers() {
const usersEndpoint = `${import.meta.env.PUBLIC_API_BASE_URL}/api/v1/users`
fetch(usersEndpoint, {
method: "GET",
headers: { "Authorization": `Bearer ${authUser.value.jwt}` }
}).then((res) => {
if (res.status === 401) {
isAuthorized.value = false
error.value = {
name: "Unauthorized",
message: "You are not authorized to access this feature."
}
} else {
res.json().then((data) => {
userList.value = data
})
}
}).catch((err) => {
error.value = err
})
}
onBeforeMount(() => {
loadUsers()
})
</script>
<template>
<USWDSAlert
v-if="error"
status="warning"
:heading="error.name"
>
{{ error.message }}
</USWDSAlert>
<div v-if="isAuthorized">
<table class="usa-table">
<thead>
<th>Name</th>
<th>Email</th>
<th>Agency</th>
</thead>
<tbody>
<tr
v-for="user in userList"
:key="user.id"
>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.agency_id }}</td>
</tr>
</tbody>
</table>
</div>
</template>
40 changes: 40 additions & 0 deletions training-front-end/src/components/AuthRedirect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!--
Process callbacks from UAA login by redirecting to the desired target page.
This component is intended to be used only by the auth_callback.astro page.
-->

<script setup>
import { ref } from 'vue'
import AuthService from '../services/auth'
import USWDSAlert from './USWDSAlert.vue'
import { useStore } from '@nanostores/vue'
import { redirectTarget, clearRedirectTarget } from '../stores/auth'
import { getUserFromTokenExchange } from '../stores/user'

const auth = await AuthService.instance()
const error = ref(null)
const authRedirectTarget = useStore(redirectTarget)

auth.loginCallback().then(async () => {
const uaaToken = await auth.getAccessToken()
await getUserFromTokenExchange(import.meta.env.PUBLIC_API_BASE_URL, uaaToken)
window.location.href = authRedirectTarget.value
clearRedirectTarget()
}).catch((err) => {
error.value = err
})
</script>

<template>
<div class="padding-top-4 padding-bottom-4 grid-container">
<div v-if="error">
<USWDSAlert
heading="Sorry, we encountered a problem while attempting to log in"
status="error"
>
{{ error }}
</USWDSAlert>
<p><a href="/">Return to Home</a></p>
</div>
</div>
</template>
41 changes: 41 additions & 0 deletions training-front-end/src/components/AuthRequired.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!--
This component can be wrapped around anything that requires authentication.
-->

<script setup>
import { useStore } from '@nanostores/vue'
import AuthService from '../services/auth'
import USWDSAlert from './USWDSAlert.vue'
import { setRedirectTarget } from '../stores/auth'
import { profile } from '../stores/user'

const auth = await AuthService.instance()
const user = useStore(profile)
const isAuthenticated = !!user.value.jwt

const handleLogin = () => {
setRedirectTarget(window.location.href)
auth.login()
}
</script>

<template>
<div class="padding-top-4 padding-bottom-4 grid-container">
<div v-if="!isAuthenticated">
<USWDSAlert
:has-heading="false"
status="error"
>
You need to sign in to use this feature.
</USWDSAlert>

<button
class="usa-button"
@click="handleLogin"
>
Sign in using SecureAuth
</button>
</div>
<slot v-if="isAuthenticated" />
</div>
</template>
11 changes: 5 additions & 6 deletions training-front-end/src/pages/admin.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroTraining from '@components/HeroTraining.astro';
import AdminSearchUser from '@components/AdminSearchUser.vue';
// import AuthRequired from '@components/AuthRequired.vue';
// import AdminIndex from '@components/AdminIndex.vue';
import AuthRequired from '@components/AuthRequired.vue';
import AdminIndex from '@components/AdminIndex.vue';
const pageTitle = "Administration Dashboard";
const pageTitle = "Admin Panel";
---
<BaseLayout title={pageTitle}>
Expand All @@ -16,8 +16,7 @@ const pageTitle = "Administration Dashboard";
</span>
</HeroTraining>
<AdminSearchUser client:only />
<!-- <AuthRequired client:only>
<AuthRequired client:only>
<AdminIndex client:only />
</AuthRequired>
-->
</BaseLayout>
</BaseLayout>
12 changes: 12 additions & 0 deletions training-front-end/src/pages/auth_callback.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
// Sets up a page at /auth_callback to catch the redirect after the user logs
// in via OAuth.
import BaseLayout from '@layouts/BaseLayout.astro';
import AuthRedirect from '@components/AuthRedirect.vue';
const pageTitle = "Login";
---
<BaseLayout title={pageTitle}>
<AuthRedirect client:only />
</BaseLayout>
Loading

0 comments on commit 0c029e3

Please sign in to comment.