This repository is associated with this code pattern overview page. In this code pattern, you use the IBM Cloud App ID service to add authentication and authorization to a Python Flask application, protecting it from unauthorized access. You then deploy the app to IBM Cloud Code Engine, a fully managed serverless platform for containerized workloads.
Although this application is written in Python Flask framework, it can be used as a reference for applications written in other programming languages as well:
- How to use IBM Cloud App ID service for authentication and authorization
- How to use IBM Cloud App ID service's auth and management API
- How to build and deploy a containerized application to IBM Cloud Code Engine service, including the details about how the service:
- Pulls your application source code from GitHub
- Builds a container image using your Dockerfile and package dependencies (requirements.txt)
- Stores the image in your IBM Cloud Container Registry namespace
- Uses secrets (and configmaps) to securely handle sensitive information
- Runs your application by fetching the container image from your Container Registry namespace
- How to develop and test your application locally by configuring the IBM Cloud App ID service and setting appropriate environment variables on your machine
This section describes how to integrate various components shown in the following architectural diagram
- You initiate the build in your IBM Cloud Code Engine project, the Code Engine fetches files from your GitHub repository to create a container image
- Upon successful completion of the build, Code Engine stores the container image in your IBM Cloud Container Registry namespace
- You create the Code Engine application, the Code Engine fetches the container image from the Container Registry namespace and tries to deploy the application. This attempt fails due to missing App ID and other details
- You configure the IBM Cloud App ID instance with the Identity Provider (IdP), redirect URI etc.
- You set environment variables that supply App ID and other details to the Code Engine application, then Code Engine application deployment succeeds
- You configure an authorization role in the App ID instance
Note: The Identity Provider may not be hosted / managed by the enterprise, it can be a Social Identity Provider supported by the IBM Cloud App ID service. Also, the GitHub Repository may not be the "GitHub Enterprise".
This code pattern requires
- IBM Cloud account
- IBM Cloud CLI with IBM Cloud Code Engine plugin and IBM Cloud Container Registry plugin
- GitHub account
python3
andpip3
if you want to run the application locally on your machine
Use ibmcloud login
CLI command to login interactively into your IBM Cloud account.
Create a new API key using following commmand and take a note of it (apikey
's value in the JSON output)
ibmcloud iam api-key-create python-appid-apikey --output json
The API key is used in some of the commands below. It is also required by the Python application.
IBM Cloud Container Registry namespace is used by the IBM Cloud Code Engine service to store your application docker image.
ibmcloud cr namespace-add <your_container_registry_namespace>
Notes
- Use -g option to set the resource group unless you want to use the default group
- The registry location where your namespace got created is displayed in output of the command. Following description assumes that the namespace was created in
us.icr.io
Create an IBM Cloud App ID service instance of the graduated tier
plan, named python-appid
. Note: Following command uses the us-south
location.
ibmcloud resource service-instance-create python-appid appid graduated-tier us-south
Note: Use -g option to set the resource group unless you want to use the default group
This section describes steps 1 and 2 in the diagram above.
Use ibmcloud target -g <resource-group>
command to set the resource group unless you want to use the default group
Create a Code Engine project named python-appid-proj
. The command automatically sets the newly created project as the current Code Engine context.
ibmcloud ce project create --name python-appid-proj
Create a credential named python-appid-us-icr-cred
for the Container Registry. It is used by Code Engine to store and retrieve container images to and from the Container Registry.
ibmcloud ce registry create --name python-appid-us-icr-cred \
--server us.icr.io \
--username iamapikey \
--password <IBM Cloud IAM API key>
Create a Code Engine build configuration named python-appid-bld
:
ibmcloud ce build create --name python-appid-bld \
--source https://github.com/IBM/python-appid-auth.git --commit main \
--image us.icr.io/<your_container_registry_namespace>/python-appid-img \
--registry-secret python-appid-us-icr-cred \
--strategy dockerfile --size small
Following information is supplied to the configuration:
--source
option specifies that the source code should be fetched from this GitHub repository. The build fetches Python source files, the Dockerfile and the requirements.txt file. requirements.txt contains list of required Python packages. The--commit
option specifies that code is to be pulled from the main branch.--image
option specifies that the build should store the image namedpython-appid-img
in the Container Registry namespace that you previously created in theus.icr.io
registry. The build accesses the registry using the credential namedpython-appid-us-icr-cred
that you created above, which is supplied using the--registry-secret
option.
Note: You can optionally specify an image tag e.g. us.icr.io/<your_container_registry_namespace>/python-appid-img:20220817-1100. If tag is not specified, the default is latest.
Next, run the actual build process.
ibmcloud ce buildrun submit --build python-appid-bld
This command fetches your files from the GitHub repository, creates a container image, and stores the container image in your IBM Cloud Container Registry namespace.
This section describes step 3 in the diagram above.
After the build is ready, you can use the container image to deploy the application. This is done by creating a Code Engine application named python-appid-app
as follows. This command fetches the container image from the Container Registry namespace specified by the --image
option, it uses the registry access credential specified by the --registry-secret
option. The --port
option specifies the port where the application listens; Flask runs on port 5000 by default:
ibmcloud ce application create --name python-appid-app \
--image us.icr.io/<your_container_registry_namespace>/python-appid-img:latest \
--registry-secret python-appid-us-icr-cred \
--port 5000 --min-scale 1
Note: If --min-scale
option (minimum number of application instances) is not specified, the default is zero. That is, the Code Engine removes all application instances if the application is not being used by anyone. That saves cost, but requires a short application startup time when scaling up from zero again. The previous command sets --min-scale
to one to avoid this delay
This command may take a few minutes. During that time, you can use the ibmcloud ce application get -n python-appid-app
command in another commandline terminal to check the application status. Here is an example output of the command:
$ ibmcloud ce application get -n python-appid-app
For troubleshooting information visit: https://cloud.ibm.com/docs/codeengine?topic=codeengine-troubleshoot-apps.
Run 'ibmcloud ce application events -n python-appid-app' to get the system events of the application instances.
Run 'ibmcloud ce application logs -f -n python-appid-app' to follow the logs of the application instances.
OK
Name: python-appid-app
ID: ...
Project Name: python-appid-proj
Project ID: ...
Age: 8m2s
...
...
Instances:
Name Revision Running Status Restarts Age
python-appid-app-00001-deployment-b5f79f4bf-s29ww python-appid-app-00001 1/3 Running 6 8m
As shown in the command output, the ibmcloud ce application create --name python-appid-app
command took more than 8 minutes and the Code Engine restarted the instance at least 6 times to try to recover out of a failure described next.
This deployment fails with an exception raise KeyError(key)
on one of the os.environ[]
statements in the application. This is because your Code Engine configuration is not fully complete yet. But in order to do that, you first need to configure the authentication in your App ID instance as described in the next section. You will need the Code Engine application URL during the App ID configuration, use the following command to get the URL:
ibmcloud ce application list
Note: This command prints the application URL even if the Code Engine deployment has failed
This section describes step 4 in the diagram above.
Use App ID documentation to setup the App ID instance named python-appid
that you created earlier:
- This Python Flask web application redirects users to the App ID instance for authentication. So configure one of several identity providers supported by the App ID service, refer to this documentation page.
- After the App ID service authenticates a user using the identity provider, it redirects the user back to the web application using a specific route called the
redirect URI
. You need to pre-registered your application's redirect URI with the App ID instance. Append/afterauth
to the Code Engine application URL that you noted down in the previous step and register the string with your App ID instance as described in this documentation page. Here is an example redirect URI:https://python-appid-app.rygjo8wa2xn.us-south.codeengine.appdomain.cloud/afterauth
. - Create an application named
python-appid-app
of typeRegular web application
containing a scope namedview
. Take a note of itsclientId
,secret
andoAuthServerUrl
attributes, you will use those to link thispython-appid-app
"App ID application" with thepython-appid-app
"Code Engine application" that you created in the previous section. Next section describes how to establish this linkage.
This section describes step 5 in the diagram above.
The Python application deployed to the Code Engine service requires App ID's clientId
and secret
which is sensitive data! So first create a Code Engine secret to store those values, as well as other sensitive information that the application requires - the session secret key and the IBM Cloud API key:
ibmcloud ce secret create --name python-appid-app-secret \
--from-literal "APPID_CLIENT_ID=<clientId string>" \
--from-literal "APPID_CLIENT_SECRET=<secret string>" \
--from-literal "SESSION_SECRET_KEY=some random string" \
--from-literal "IBM_CLOUD_APIKEY=<IAM API key string>"
Next, you provide the secret to the Code Engine application using the --env-from-secret
option. Additionally, the Python application requires the redirect URI too because it needs to send the redirect URI to the App ID service during OIDC protocol exchanges. The application also requires value of the oAuthServerUrl
attribute of the App ID application that you created in the previous section. Following command uses the --env
option to supply redirect URI and OAuth server details:
ibmcloud ce application update --name python-appid-app \
--env-from-secret python-appid-app-secret \
--env "APPID_REDIRECT_URI=<redirect URI string>" \
--env "APPID_OAUTH_SERVER_URL=<oAuthServerUrl string>"
Now you have completed configuration of the Code Engine application. The Code Engine will automatically create a new revision, and the deployment will be successful. Run the application URL in your browser. You will be redirected to the identity provider that you have configured in the App ID service, and after you login successfully, you will be redirected back to the application.
Yay! ... but ...
... the application will display the message "Unauthorized!" This is expected, because you have not configured "authorization" in your App ID instance yet! Next section describes how to do that by creating and assigning App ID roles.
This section describes step 6 in the diagram above.
In a previous step, you created an App ID application named python-appid-app
that has a scope named view
. App ID service lets you define a role that lists one or more scopes in one or more applications that the role is authorized for. Then you assign one or more roles to a user.
Refer to App ID documentation to create and assign a role:
- Create a role named
user
containing the scopepython-appid-app/view
- Assign the
user
role to your own user profile
Now refresh the browser where your application is running or copy-paste the application URL in your browser or click on the "Open URL" link in the Code Engine web UI. You will be taken through a couple of redirects as before, and this time around, the application will display the message "This route requires authentication and authorization - Powered by IBM Cloud App ID!".
YAY!!!
As explained later, the AppIDAuthProvider class (defined in auth.py) - which has all of the App ID service integration / interactions logic - is designed such that you can selectively enable the auth check for specific routes. To try this, launch a separate private incongnito browser window and run the /noauth_route
of the application. Notice that the application doesn't redirect to the identity provider this time, it just displays the message "This route is open to all!".
Note that now the application won't redirect to the identity provider even if you change the route back to /auth_route
. This is because, as described in a subsequent section, the logic in the AppIDAuthProvider class automatically stores the access token in the user session. The application won't redirect to the identity provider until the token is valid, default validity is one hour. If you try the /auth_route
after validity period of the access token, then the application will be redirected to the identity provider and the new access token will get stored again in the user session.
You performed a number of setup steps thus far to get this application working. If you push any updates to the GitHub source code, just run following commands to redeploy the application to Code Engine.
First, login using ibmcloud login
command. Then optionally select a non-default resource group using ibmcloud target -g
command and select your Code Engine project using the ibmcloud ce project select --name python-appid-proj
command.
-
If you want to specify a new tag for newly updated container image, update the build definition as follows. For example, replace in the command below by current datetime string:
Note: Ignore this step if you don't want to change the image tag
ibmcloud ce build update --name python-appid-bld \ --image us.icr.io/<your_container_registry_namespace>/python-appid-img:<new tag>
-
Then run the build:
ibmcloud ce buildrun submit --build python-appid-bld
-
After the build is ready, redeploy the application using the new container image:
ibmcloud ce application update --name python-appid-app \ --image us.icr.io/<your_container_registry_namespace>/python-appid-img:<new tag>
Note: Of course, if you did not update the build definition in step 1 to specify an explicit tag for the new container image, then do not use the
--image
option in the command above. Just run the commandibmcloud ce application update --name python-appid-app
For developing and testing this application locally on your machine, you need to add one more redirect URI to your App ID instance, set a few environment variables and install two packages that your Python application requires:
-
Register an additional redirect URI
http://0.0.0.0:5000/afterauth
with your App ID instance as described in this documentation page -
Note down
clientId
,secret
andoAuthServerUrl
values of yourpython-appid-app
"App ID application" that you created earlier. Set following environment variables:- Set
APPID_CLIENT_ID
environment variable to the value of theclientId
key - Set
APPID_CLIENT_SECRET
environment variable to the value of thesecret
key - Set
APPID_OAUTH_SERVER_URL
environment variable to the value of theoAuthServerUrl
key - Set
APPID_REDIRECT_URI
environment variable to "http://0.0.0.0:5000/afterauth" - Set
SESSION_SECRET_KEY
environment variable to "some random string" - Set
IBM_CLOUD_APIKEY
environment variable to the value of your IBM Cloud API key
- Set
-
Install package dependencies:
pip3 install flask pip3 install requests
-
Run the application using
python3 app.py
Refer to this page which describes App ID concepts to learn the basics like authentication, authorization, OAuth 2.0 and OIDC.
Following diagram shows authentication and authorization flow. It uses the OIDC protocol.
Note: This diagram does not contain GitHub repository and IBM Cloud Container registry because those are used only during application deployment
- User types an application URL that requires auth in the browser, for example the URL https://python-appid-app.rygjo8wa2xn.us-south.codeengine.appdomain.cloud/auth_route
- Application's backend running in the IBM Cloud Code Engine checks whether user session has a non-expired access token. (See
_is_auth_active()
method in theAppIDAuthProvider
class description below, this method gets invoked due to the@auth.check
decorator on the route definition in app.py) - If the user session doesn't have access token or the token is expired, the browser is redirected to IBM Cloud App ID's
/authorization
endpoint. (Seestart_auth()
method in theAppIDAuthProvider
class, this method also gets invoked from@auth.check
decorator's definition) - App ID in turn redirects the browser to the Identity Provider that you have configured in your App ID instance. User provides login credentials to authenticate with the Identity Provider
- Upon successful user login, App ID redirects the browser to application's redirect-URI that you preregistered with your App ID instance
- Application's backend running in the Code Engine retrieves user's access token and role(s) from the App ID. (See
after_auth()
method in theAppIDAuthProvider
class) - Then the browser is redirected to the original URL that the user had typed in the step 1 above
- Similar to step 2, application's backend running in the Code Engine checks whether user session has a non-expired access token. This time it does. It then checks whether the user session also has a role (See
_user_has_a_role()
method in theAppIDAuthProvider
class), and finally the URL works!
The AppIDAuthProvider
class defined in auth.py contains all the App ID service integration / interaction logic. It uses the OIDC protocol as described here in App ID documentation.
-
It creates a
Flask
instance and stores it in an instance variable namedflask
:self.flask = Flask(__name__)
-
Its
start_auth()
method redirects the application to App ID service's authorization endpoint. It sets appropriate query parameter values during the redirect such asclient_id
andredirect_uri
. It also sets the query parameterresponse_type
to the valuecode
and the parameterscope
to the valueopenid
as shown below:return redirect("{}?client_id={}&response_type=code&redirect_uri={}&scope=openid".format(authorization_endpoint, cls.CLIENT_ID, cls.REDIRECT_URI))
-
It defines the
/afterauth
route which is registered with the App ID service as the "redirect URI" as described earlier:@self.flask.route("/afterauth") def after_auth(): ...
After the user is authenticated, App ID service redirects back to this application endpoint. The
after_auth()
method performs following tasks:-
It sends the authorization code to the token endpoint to retrieve
access_token
andid_token
resp = requests.post(token_endpoint, ...
It stores the
access_token
in Flask'ssession
object. -
It uses the
_get_user_info()
helper method to retrive user's ID and email from theid_token
user_email, user_id = AppIDAuthProvider._get_user_info(resp_json["id_token"])
-
It uses the
_get_user_roles()
helper method to retrieve user's roles using the user's IDresp_json = AppIDAuthProvider._get_user_roles(user_id)
It stores the roles in Flask's
session
object.Note: If you want to add fine-grained authorization that checks whether the logged in user has particular application scope(s), then modify this logic to retrieve details about the roles. Refer to APP ID Roles API.
-
Finally, the
/afterauth
route redirects the application to the route that had originally redirected the appication to the authorization endpoint:endpoint_context = session.pop(AppIDAuthProvider.ENDPOINT_CONTEXT, None) return redirect(endpoint_context)
-
-
The
AppIDAuthProvider
class uses Flask'ssession
to store user'saccess_token
and roles.session[AppIDAuthProvider.APPID_USER_TOKEN] = access_token session[AppIDAuthProvider.APPID_USER_ROLES] = resp_json["roles"]
Before redirecting the application to the authorization endpoint, the application logic checks whether
access_token
already exists in the session and whether it is still valid (default validity of App ID tokens is 1 hour). This provides better user experience because application is not redirected to the authorization endpoint if validaccess_token
is already present in the session. Validity ofaccess_token
is checked using App ID service'sintrospect
endpoint as described next. -
The
_is_auth_active()
method uses theintrospect
API provided by the App ID service to check whether theaccess_token
stored in the session is still valid or notresp = requests.post(introspect_endpoint, data = {"token": token}, auth = HTTPBasicAuth(cls.CLIENT_ID, cls.CLIENT_SECRET)) resp_json = resp.json() if "active" in resp_json and resp_json["active"]: return True, "" ...
-
The
AppIDAuthProvider
class defines thecheck()
method that performs user authentication and authorization Following code checks whether the authentication is active / valid:auth_active, err_msg = cls._is_auth_active() if not auth_active: ...
Following code checks the authorization:
if not cls._user_has_a_role(): return "Unauthorized!" ...
Note: Modify this logic if you want to add fine-grained authorization that checks whether the logged in user has specific role(s) or specific application scope(s)
The
check()
method is implicitly invoked using the "decorator" feature of the Python language as described below.
The app.py file contains Flask routes. The logic in this file is very straight forward because the AppIDAuthProvider
class in auth.py does all the auth heavy lifting as described in the previous section.
To start with, the AppIDAuthProvider
class is imported from auth.py and an instance of the class is stored in a variable named auth
. Also, the flask
instance-variable of the class is stored in a variable named flask
for improved readability of the subsequent code:
from auth import AppIDAuthProvider
auth = AppIDAuthProvider()
flask = auth.flask
It then defines a default route using the idiomatic Flask route
decorator:
@flask.route("/")
def index():
return redirect("/auth_route")
The index()
function associated with the /
route simply redirects to the /auth_route
so that when you click on the Code Engine application URL, the application just works; you don't need to additionally type a route in your browser. If the /
route is not defined, you would get 404 Not Found error when you click the Code Engine application URL.
Next, it defines the /auth_route
using the @flask.route("/auth_route")
decorator on the auth_route()
function. But notice that there is one more decorator, @auth.check
, on this function:
@flask.route("/auth_route")
@auth.check
def auth_route():
return "This route requires authentication and authorization - Powered by IBM Cloud App ID!"
The @auth.check
decorator implicitely invokes the check()
method of the AppIDAuthProvider
class that triggers authentication and authorization flow as previously described. Accordingly, the function returns an informative message.
The application defines one more route using the @flask.route("/noauth_route")
decorator on the noauth_route()
function:
@flask.route("/noauth_route")
def noauth_route():
return "This route is open to all!"
As announced by its return value, this route is unauthenticated. This is because it doesn't have the additional @auth.check
decorator.
Finally, the application runs the Flask instance at the default 5000 port:
if __name__ == "__main__":
flask.run(host="0.0.0.0")
Run following commands in the IBM Cloud CLI to remove the resources that you created in this code pattern:
- Remove the IBM Cloud Container Registry namespace:
ibmcloud cr namespace-rm <your_container_registry_namespace>
Note: Use -g option to specify the resource group if you used that option when you created the namespace
- Delete the IBM Cloud App ID service instance:
ibmcloud resource service-instance-delete python-appid
Note: Use -g option to specify the resource group if you used that option when you created the App ID instance
- Delete the IBM Cloud Code Engine project:
First use the ibmcloud target -g <resource-group>
command to set the resource group unless you had created the project in the default group.
Then use the following command to delete the project:
ibmcloud ce project delete --name python-appid-proj --hard
Note: If you do not specify the --hard
option, Code Engine allows you to restore the project within a few days after the 'soft' delete
In this code pattern IBM Cloud App ID service was used to add authentication and authorization to a Python web application that uses the Flask framework. This code pattern has a simple authorization check: does the user have at least one role defined in App ID? You can update the logic to make it stricter. For example, check whether the user has a particular, application-scoped role. Refer to the IBM Cloud API Docs for retrieving details of a specific role. Also, read about the App ID refresh token to see whether you can update the authentication logic described in this code pattern to improve your user login experience.
The code pattern also described deployment of the application to IBM Cloud Code Engine, a fully managed serverless platform for containerized applications. This code pattern uses IBM Cloud CLI to interact with the Code Engine service, but you can also use the Code Engine UI. Open the UI and explore the python-appid-proj
to see the artifacts created by ibmcloud ce registry create
, ibmcloud ce build create
, ibmcloud ce buildrun submit
, ibmcloud ce application create
, and ibmcloud ce secret create
CLI commands in this code pattern.
The IBM Cloud Code Engine service enables you to quickly deploy your application to make it available to your end users, and the IBM Cloud App ID service protects your application from unauthorized access.
This code pattern is licensed under the Apache License, Version 2. Separate third-party code objects invoked within this code pattern are licensed by their respective providers pursuant to their own separate licenses. Contributions are subject to the Developer Certificate of Origin, Version 1.1 and the Apache License, Version 2.