Problem Statement
AWS Cognito provides support for User Authentication, using it you can provision a hosted authentication UI that you can add to your app to handle sign-up and sign-in workflows. Amazon Cognito’s hosted UI can be used as a feature to sign in directly to your user pool through Facebook, Amazon, Google, and Apple as well as through OpenID Connect (OIDC) and SAML identity providers.
Our goal here is to display a pre-built hosted UI that redirects to a social sign-in provider of AWS Cognito user pool. After a user successfully authenticates with user pool, AWS Amplify creates a new user in your user pool if needed, and it provides the user’s OIDC token to access your app. We will pass the token as received to REST API in header. Backend server should validate token and allow/deny the calls.
Tech Stack
- React (frontend)
- Go (Backend REST APIs)
- AWS Cognito (Authentication)
- Amplify SDK (Javascript SDK for Cognito Integration)
- Axios (Http calls)
Command to Run
Start Frontend
- Navigate to src/samplewebapp/client
- npm start
- Should start server in localhost:3000
Start Backend
- Navigate to src/samplewebapp
- go run main.go
- Should start server in localhost:8080
- API supported localhost:8080/api/task
- Test API supported localhost:8080/hello (print Hello World)
High Level Design Diagram
Solution
Setup AWS Cognito User Pool
- Create AWS Cognito User Pool.
- Set the user pool attributes.
- Set Callback URLs and SignOut URLs. It should exactly be same as used in the client side while calling Signin page.
- Create User to test signin feature from the App.
Create React Based Front End Application
- We will use React framework to create front end application.
- Run this command to check the GOPATH env variable.
echo $GOPATH
- If it is not set then use export command to set the GOPATH variable.
export GOPATH="/Sample/Path/Location"
- Create Directory structure under Go Path.
mkdir src
mkdir pkg
mkdir bin - Create React Application and launch
npx create-react-app client
cd client
npm start - Above will start the application on port 3000.
localhost:3000
Create GoLang API server
- Create main.go file
touch main.go
- Create Routes for supported APIs
package main
import ( "fmt" "net/http" middleware "samplewebapp/Middleware"
"github.com/gorilla/mux")
// The new router function creates the router and// returns it to us. We can now use this function// to instantiate and test the router outside of the main functionfunc newRouter() *mux.Router { r := mux.NewRouter() r.HandleFunc("/hello", handler).Methods("GET")
// Declare the static file directory and point it to the directory we just made staticFileDirectory := http.Dir("./assets/") // Declare the handler, that routes requests to their respective filename. // The fileserver is wrapped in the `stripPrefix` method, because we want to // remove the "/assets/" prefix when looking for files. // For example, if we type "/assets/index.html" in our browser, the file server // will look for only "index.html" inside the directory declared above. // If we did not strip the prefix, the file server would look for "./assets/assets/index.html", and yield an error staticFileHandler := http.StripPrefix("/assets/", http.FileServer(staticFileDirectory)) // The "PathPrefix" method acts as a matcher, and matches all routes starting // with "/assets/", instead of the absolute route itself r.PathPrefix("/assets/").Handler(staticFileHandler).Methods("GET")
r.HandleFunc("/api/task", (middleware.GetAllTask)).Methods("GET") r.HandleFunc("/api/task", (middleware.CreateTask)).Methods("POST") r.HandleFunc("/api/task", middleware.OptionsTask).Methods("OPTIONS")
return r}func main() {
// The router is now formed by calling the `newRouter` constructor function // that we defined above. The rest of the code stays the same r := newRouter()
http.ListenAndServe(":8080", r)
}
func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World!")}
- Install routing libraries
dep ensure -add github.com/gorilla/mux - Add Middleware.go for handling the API calls
// CreateTask create task routefunc CreateTask(w http.ResponseWriter, r *http.Request) { w.Header().Set("Context-Type", "application/x-www-form-urlencoded") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Access-Control-Allow-Headers", "Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } var task Subscription _ = json.NewDecoder(r.Body).Decode(&task) // fmt.Println(task, r.Body) insertOneTask(w, r, task) json.NewEncoder(w).Encode(task)}
Launch Cognito Hosted Signin UI
- To Launch Cognito Hosted signin UI, we will integrate with AWS Cognito using Amplify Library.
- Install Amplify framework
npm i aws-amplify aws-amplify-react
- Configure AWS Amplify. Create aws-exports.js in the /src directory:
Values for UserPoolId and UserPoolWebClientId is available in AWS Cognito web page.
const awsmobile = { Auth: { // Amazon Cognito Region region: "us-east-1", // Amazon Cognito User Pool ID userPoolId: "us-east-1_XXXXXX", // Amazon Cognito Web Client ID (26-char alphanumeric string) userPoolWebClientId: "ft374gsb7mlau9nhbYYYYYYY", oauth: { domain: "vikas-test-tool.auth.us-east-1.amazoncognito.com", scope:[ 'phone', 'email', 'openid', 'profile', 'aws.cognito.signin.user.admin' ], redirectSignIn: "http://localhost:3000/", redirectSignOut: "http://localhost:3000/signout.js", responseType: "code" }
} }; export default awsmobile;
- Update App.js file to call hosted UI.
- Add required Imports
import Amplify from 'aws-amplify';import awsmobile from './aws-exports';import { withAuthenticator } from 'aws-amplify-react';import { Auth, Hub,API } from 'aws-amplify'
- Add Withauthenticator attribute
//export default App;export default withAuthenticator(App, true);
- In the Function App, add the support for Hub.listen to listen for authentication events.
function App(props) { // in useEffect, we create the listener useEffect(() => { Hub.listen('auth', (data) => { const { payload } = data console.log('A new auth event has happened: ', data) if (payload.event === 'signIn') { console.log('a user has signed in!') } if (payload.event === 'signOut') { console.log('a user has signed out!') } }) }, []) return ( <div className="App"> <header className="App-header"> <p> Sample Full Stack App. </p> <button onClick={checkUser}>Check User</button> <button onClick={signOut}>Sign Out</button> <button onClick={GetTaskAPI}>Get Task</button>
</header> </div> );}
- Start Application using “npm start”
- It should launch Cognito Hosted UI
- Signin using the user credentials created in AWS Cognito console for User pool.
Call REST API from React Frontend
- I am using axios library to call REST API from React framework
// If not, get the token from aws-amplify: const user = await Auth.currentAuthenticatedUser(); const token = user.signInUserSession.idToken.jwtToken;
axios.get("http://localhost:8080/api/task", { crossdomain: true, headers: { 'Authorization': 'Bearer ' + token},}).then(res => { console.log(res);}).catch(error => { console.log('error', error);})
Add Authentication Header and CORS
- Please check the code above.
- Below line adds the CORS flag in the request attribute
crossdomain: true
- To add authentication header, we are using “headers” attribute.
Validate Bearer Token in GoLang
- Non-simple CORS requests via AJAX are pre-flighted. This is a browser behavior and nothing specific to axios. There’s nothing inherently wrong with this behavior and if it’s working for you, you can just leave it.
- Basically the
OPTIONS
request is used to check if you are allowed to perform theGET
request from that domain and what headers can be used for that request. This is not axios specific. - I was getting the same error, so i need to define “OPTIONS” value in Golang for API handler.
- We can parse the token from request using “github.com/dgrijalva/jwt-go”. And validate it against Cognito SDK APIs before allowing access of API.
## User Groups feature of Cognito to manage access of Resource Server
- You can create and manage groups in a user pool from the AWS Management Console, the APIs, and the CLI. As a developer (using AWS credentials), you can create, read, update, delete, and list the groups for a user pool. You can also add users and remove users from groups.
- We are using groups in a user pool to control permission with Resource server written in GoLang. The groups that a user is a member of are included in the ID token provided by a user pool when a user signs in.
- One or many gorups can be assigned to a user. Based on groups value in JWT, we can provide access of resources to user.
- Create User group
-Assign User to a Group
- cognito:group array in JWT payload define the Group association
- cognito:groups value as admin in logs
Test Feature
- Open developer tool in Chrome/ or other browser
- Capture Console and Network logs
- Home Page
- Click on SignIn with AWS button
- It should launch Cognito Hosted Sign In Page.
- Enter the User credentials in Sign In Page. User should already be created in Cognito User Pool. Username — test4@test.com, Pwd — Welcome123!
- If successfully signed in then user will see option to “Check User”, “Sign Out”, “Get Task”
- Check User : it will print the user details in Console for developer tools. Sign Out : It will sign out user from Cognito User pool. Get Task : It will call backend server with sample “Get” and “Post” call
- Check User : Console log
- Get Task : Console log
- Get Task : Network log