From ec91fa55db649621ced67ad1393b713ec2c0a638 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Wed, 15 Jul 2020 04:55:55 -0700 Subject: [PATCH] docs: Add more STS docs with dex and python example (#10047) --- docs/sts/.gitignore | 104 +++++++++++++++++++ docs/sts/README.md | 2 +- docs/sts/client-grants.go | 4 +- docs/sts/client-grants.py | 48 +++++++++ docs/sts/client_grants/__init__.py | 144 ++++++++++++++++++++++++++ docs/sts/client_grants/sts_element.py | 90 ++++++++++++++++ docs/sts/dex.md | 98 ++++++++++++++++++ docs/sts/dex.yaml | 78 ++++++++++++++ docs/sts/keycloak.md | 8 +- docs/sts/web-identity.go | 10 +- docs/sts/web-identity.py | 111 ++++++++++++++++++++ 11 files changed, 685 insertions(+), 12 deletions(-) create mode 100644 docs/sts/.gitignore create mode 100644 docs/sts/client-grants.py create mode 100644 docs/sts/client_grants/__init__.py create mode 100644 docs/sts/client_grants/sts_element.py create mode 100644 docs/sts/dex.md create mode 100644 docs/sts/dex.yaml create mode 100644 docs/sts/web-identity.py diff --git a/docs/sts/.gitignore b/docs/sts/.gitignore new file mode 100644 index 000000000..894a44cc0 --- /dev/null +++ b/docs/sts/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/docs/sts/README.md b/docs/sts/README.md index f5436880f..a826a3138 100644 --- a/docs/sts/README.md +++ b/docs/sts/README.md @@ -14,7 +14,7 @@ Following are advantages for using temporary credentials: |AuthN | Description | | :---------------------- | ------------------------------------------ | | [**Client grants**](https://github.com/minio/minio/blob/master/docs/sts/client-grants.md) | Let applications request `client_grants` using any well-known third party identity provider such as KeyCloak, Okta. This is known as the client grants approach to temporary access. Using this approach helps clients keep MinIO credentials to be secured. MinIO STS supports client grants, tested against identity providers such as KeyCloak, Okta. | -| [**WebIdentity**](https://github.com/minio/minio/blob/master/docs/sts/web-identity.md) | Let users request temporary credentials using any OpenID(OIDC) compatible web identity providers such as KeyCloak, Facebook, Google etc. | +| [**WebIdentity**](https://github.com/minio/minio/blob/master/docs/sts/web-identity.md) | Let users request temporary credentials using any OpenID(OIDC) compatible web identity providers such as KeyCloak, Dex, Facebook, Google etc. | | [**AssumeRole**](https://github.com/minio/minio/blob/master/docs/sts/assume-role.md) | Let MinIO users request temporary credentials using user access and secret keys. | | [**AD/LDAP**](https://github.com/minio/minio/blob/master/docs/sts/ldap.md) | Let AD/LDAP users request temporary credentials using AD/LDAP username and password. | diff --git a/docs/sts/client-grants.go b/docs/sts/client-grants.go index 605fac8b3..13dce5010 100644 --- a/docs/sts/client-grants.go +++ b/docs/sts/client-grants.go @@ -1,7 +1,7 @@ // +build ignore /* - * MinIO Cloud Storage, (C) 2018 MinIO, Inc. + * MinIO Cloud Storage, (C) 2019,2020 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ var ( func init() { flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint") - flag.StringVar(&idpEndpoint, "idp-ep", "https://localhost:9443/oauth2/token", "IDP endpoint") + flag.StringVar(&idpEndpoint, "idp-ep", "http://localhost:8080/auth/realms/minio/protocol/openid-connect/token", "IDP token endpoint") flag.StringVar(&clientID, "cid", "", "Client ID") flag.StringVar(&clientSecret, "csec", "", "Client secret") } diff --git a/docs/sts/client-grants.py b/docs/sts/client-grants.py new file mode 100644 index 000000000..7b3803938 --- /dev/null +++ b/docs/sts/client-grants.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import boto3 +from boto3.session import Session +from botocore.session import get_session + +from client_grants import ClientGrantsCredentialProvider + +boto3.set_stream_logger('boto3.resources', logging.DEBUG) + +bc_session = get_session() +bc_session.get_component('credential_provider').insert_before( + 'env', + ClientGrantsCredentialProvider('NZLOOFRSluw9RfIkuHGqfk1HFp4a', + '0Z4VTG8uJBSekn42HE40DK9vQb4a'), +) + +boto3_session = Session(botocore_session=bc_session) +s3 = boto3_session.resource('s3', endpoint_url='http://localhost:9000') + +with open('/etc/hosts', 'rb') as data: + s3.meta.client.upload_fileobj(data, + 'testbucket', + 'hosts', + ExtraArgs={'ServerSideEncryption': 'AES256'}) + +# Upload with server side encryption, using temporary credentials +s3.meta.client.upload_file('/etc/hosts', + 'testbucket', + 'hosts', + ExtraArgs={'ServerSideEncryption': 'AES256'}) + +# Download encrypted object using temporary credentials +s3.meta.client.download_file('testbucket', 'hosts', '/tmp/hosts') diff --git a/docs/sts/client_grants/__init__.py b/docs/sts/client_grants/__init__.py new file mode 100644 index 000000000..bcf726500 --- /dev/null +++ b/docs/sts/client_grants/__init__.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +# standard. +import os + +import certifi +# Dependencies +import urllib3 +from botocore.credentials import CredentialProvider, RefreshableCredentials +from botocore.exceptions import CredentialRetrievalError +from dateutil.parser import parse + +from .sts_element import STSElement + + +class ClientGrantsCredentialProvider(CredentialProvider): + """ + ClientGrantsCredentialProvider implements CredentialProvider compatible + implementation to be used with boto_session + """ + METHOD = 'assume-role-client-grants' + CANONICAL_NAME = 'AssumeRoleClientGrants' + + def __init__(self, cid, csec, + idp_ep='http://localhost:8080/auth/realms/minio/protocol/openid-connect/token', + sts_ep='http://localhost:9000'): + self.cid = cid + self.csec = csec + self.idp_ep = idp_ep + self.sts_ep = sts_ep + + # Load CA certificates from SSL_CERT_FILE file if set + ca_certs = os.environ.get('SSL_CERT_FILE') + if not ca_certs: + ca_certs = certifi.where() + + self._http = urllib3.PoolManager( + timeout=urllib3.Timeout.DEFAULT_TIMEOUT, + maxsize=10, + cert_reqs='CERT_NONE', + ca_certs=ca_certs, + retries=urllib3.Retry( + total=5, + backoff_factor=0.2, + status_forcelist=[500, 502, 503, 504] + ) + ) + + def load(self): + """ + Search for credentials with client_grants + """ + if self.cid is not None: + fetcher = self._create_credentials_fetcher() + return RefreshableCredentials.create_from_metadata( + metadata=fetcher(), + refresh_using=fetcher, + method=self.METHOD, + ) + else: + return None + + def _create_credentials_fetcher(self): + method = self.METHOD + + def fetch_credentials(): + # HTTP headers are case insensitive filter out + # all duplicate headers and pick one. + headers = {} + headers['content-type'] = 'application/x-www-form-urlencoded' + headers['authorization'] = urllib3.make_headers( + basic_auth='%s:%s' % (self.cid, self.csec))['authorization'] + + response = self._http.urlopen('POST', self.idp_ep, + body="grant_type=client_credentials", + headers=headers, + preload_content=True, + ) + if response.status != 200: + message = "Credential refresh failed, response: %s" + raise CredentialRetrievalError( + provider=method, + error_msg=message % response.status, + ) + + creds = json.loads(response.data) + + query = {} + query['Action'] = 'AssumeRoleWithClientGrants' + query['Token'] = creds['access_token'] + query['DurationSeconds'] = creds['expires_in'] + query['Version'] = '2011-06-15' + + query_components = [] + for key in query: + if query[key] is not None: + query_components.append("%s=%s" % (key, query[key])) + + query_string = '&'.join(query_components) + sts_ep_url = self.sts_ep + if query_string: + sts_ep_url = self.sts_ep + '?' + query_string + + response = self._http.urlopen( + 'POST', sts_ep_url, preload_content=True) + if response.status != 200: + message = "Credential refresh failed, response: %s" + raise CredentialRetrievalError( + provider=method, + error_msg=message % response.status, + ) + + return parse_grants_response(response.data) + + def parse_grants_response(data): + """ + Parser for AssumeRoleWithClientGrants response + + :param data: Response data for AssumeRoleWithClientGrants request + :return: dict + """ + root = STSElement.fromstring( + 'AssumeRoleWithClientGrantsResponse', data) + result = root.find('AssumeRoleWithClientGrantsResult') + creds = result.find('Credentials') + return dict( + access_key=creds.get_child_text('AccessKeyId'), + secret_key=creds.get_child_text('SecretAccessKey'), + token=creds.get_child_text('SessionToken'), + expiry_time=parse(creds.get_child_text('Expiration')).isoformat()) + + return fetch_credentials diff --git a/docs/sts/client_grants/sts_element.py b/docs/sts/client_grants/sts_element.py new file mode 100644 index 000000000..9ad7c3f8e --- /dev/null +++ b/docs/sts/client_grants/sts_element.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from xml.etree import cElementTree +from xml.etree.cElementTree import ParseError + +if hasattr(cElementTree, 'ParseError'): + _ETREE_EXCEPTIONS = (ParseError, AttributeError, ValueError, TypeError) +else: + _ETREE_EXCEPTIONS = (SyntaxError, AttributeError, ValueError, TypeError) + +_STS_NS = {'sts': 'https://sts.amazonaws.com/doc/2011-06-15/'} + + +class STSElement(object): + """STS aware XML parsing class. Wraps a root element name and + cElementTree.Element instance. Provides STS namespace aware parsing + functions. + + """ + + def __init__(self, root_name, element): + self.root_name = root_name + self.element = element + + @classmethod + def fromstring(cls, root_name, data): + """Initialize STSElement from name and XML string data. + + :param name: Name for XML data. Used in XML errors. + :param data: string data to be parsed. + :return: Returns an STSElement. + """ + try: + return cls(root_name, cElementTree.fromstring(data)) + except _ETREE_EXCEPTIONS as error: + raise InvalidXMLError( + '"{}" XML is not parsable. Message: {}'.format( + root_name, error.message + ) + ) + + def findall(self, name): + """Similar to ElementTree.Element.findall() + + """ + return [ + STSElement(self.root_name, elem) + for elem in self.element.findall('sts:{}'.format(name), _STS_NS) + ] + + def find(self, name): + """Similar to ElementTree.Element.find() + + """ + elt = self.element.find('sts:{}'.format(name), _STS_NS) + return STSElement(self.root_name, elt) if elt is not None else None + + def get_child_text(self, name, strict=True): + """Extract text of a child element. If strict, and child element is + not present, raises InvalidXMLError and otherwise returns + None. + + """ + if strict: + try: + return self.element.find('sts:{}'.format(name), _STS_NS).text + except _ETREE_EXCEPTIONS as error: + raise InvalidXMLError( + ('Invalid XML provided for "{}" - erroring tag <{}>. ' + 'Message: {}').format(self.root_name, name, error.message) + ) + else: + return self.element.findtext('sts:{}'.format(name), None, _STS_NS) + + def text(self): + """Fetch the current node's text + + """ + return self.element.text diff --git a/docs/sts/dex.md b/docs/sts/dex.md new file mode 100644 index 000000000..72f9bc304 --- /dev/null +++ b/docs/sts/dex.md @@ -0,0 +1,98 @@ +# Dex Quickstart Guide [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io) + +Dex is an identity service that uses OpenID Connect to drive authentication for apps. Dex acts as a portal to other identity providers through "connectors." This lets dex defer authentication to LDAP servers, SAML providers, or established identity providers like GitHub, Google, and Active Directory. Clients write their authentication logic once to talk to dex, then dex handles the protocols for a given backend. + +## Prerequisites + +Install Dex by following [Dex Getting Started Guide](https://github.com/dexidp/dex/blob/master/Documentation/getting-started.md) + +### Start Dex + +``` +~ ./bin/dex serve dex.yaml +time="2020-07-12T20:45:50Z" level=info msg="config issuer: http://127.0.0.1:5556/dex" +time="2020-07-12T20:45:50Z" level=info msg="config storage: sqlite3" +time="2020-07-12T20:45:50Z" level=info msg="config static client: Example App" +time="2020-07-12T20:45:50Z" level=info msg="config connector: mock" +time="2020-07-12T20:45:50Z" level=info msg="config connector: local passwords enabled" +time="2020-07-12T20:45:50Z" level=info msg="config response types accepted: [code token id_token]" +time="2020-07-12T20:45:50Z" level=info msg="config using password grant connector: local" +time="2020-07-12T20:45:50Z" level=info msg="config signing keys expire after: 3h0m0s" +time="2020-07-12T20:45:50Z" level=info msg="config id tokens valid for: 3h0m0s" +time="2020-07-12T20:45:50Z" level=info msg="listening (http) on 0.0.0.0:5556" +``` + +### Configure MinIO server with Dex +``` +~ export MINIO_IDENTITY_OPENID_CLAIM_NAME=name +~ export MINIO_IDENTITY_OPENID_CONFIG_URL=http://127.0.0.1:5556/dex/.well-known/openid-configuration +~ minio server ~/test +``` + +### Run the `web-identity.go` + +``` +~ go run web-identity.go -cid example-app -csec ZXhhbXBsZS1hcHAtc2VjcmV0 \ + -config-ep http://127.0.0.1:5556/dex/.well-known/openid-configuration \ + -cscopes groups,openid,email,profile +``` + +``` +~ mc admin policy add admin allaccess.json +``` + +Contents of `allaccess.json` +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:aws:s3:::*" + ] + } + ] +} +``` + +### Visit http://localhost:8080 +You will be redirected to dex login screen - click "Login with email", enter username password +> username: admin@example.com +> password: password + +and then click "Grant access" + +On the browser now you shall see the list of buckets output, along with your temporary credentials obtained from MinIO. +``` +{ + "buckets": [ + "dl.minio.equipment", + "dl.minio.service-fulfillment", + "testbucket" + ], + "credentials": { + "AccessKeyID": "Q31CVS1PSCJ4OTK2YVEM", + "SecretAccessKey": "rmDEOKARqKYmEyjWGhmhLpzcncyu7Jf8aZ9bjDic", + "SessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJRMzFDVlMxUFNDSjRPVEsyWVZFTSIsImF0X2hhc2giOiI4amItZFE2OXRtZEVueUZaMUttNWhnIiwiYXVkIjoiZXhhbXBsZS1hcHAiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6IjE1OTQ2MDAxODIiLCJpYXQiOjE1OTQ1ODkzODQsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTU1Ni9kZXgiLCJuYW1lIjoiYWRtaW4iLCJzdWIiOiJDaVF3T0dFNE5qZzBZaTFrWWpnNExUUmlOek10T1RCaE9TMHpZMlF4TmpZeFpqVTBOallTQld4dlkyRnMifQ.nrbzIJz99Om7TvJ04jnSTmhvlM7aR9hMM1Aqjp2ONJ1UKYCvegBLrTu6cYR968_OpmnAGJ8vkd7sIjUjtR4zbw", + "SignerType": 1 + } +} +``` + +Now you have successfully configured Dex IdP with MinIO. + +> NOTE: Dex supports groups with external connectors so you can use `groups` as policy claim instead of `name`. +``` +export MINIO_IDENTITY_OPENID_CLAIM_NAME=groups +``` + +and add relevant policies on MinIO using `mc admin policy add myminio/ group-access.json` + +## Explore Further + +- [MinIO STS Quickstart Guide](https://docs.min.io/docs/minio-sts-quickstart-guide) +- [The MinIO documentation website](https://docs.min.io) diff --git a/docs/sts/dex.yaml b/docs/sts/dex.yaml new file mode 100644 index 000000000..20b818965 --- /dev/null +++ b/docs/sts/dex.yaml @@ -0,0 +1,78 @@ +# The base path of dex and the external name of the OpenID Connect service. +# This is the canonical URL that all clients MUST use to refer to dex. If a +# path is provided, dex's HTTP service will listen at a non-root URL. +issuer: http://127.0.0.1:5556/dex + +# The storage configuration determines where dex stores its state. Supported +# options include SQL flavors and Kubernetes third party resources. +# +# See the storage document at Documentation/storage.md for further information. +storage: + type: sqlite3 + config: + file: examples/dex.db + +# Configuration for the HTTP endpoints. +web: + http: 0.0.0.0:5556 + # Uncomment for HTTPS options. + # https: 127.0.0.1:5554 + # tlsCert: /etc/dex/tls.crt + # tlsKey: /etc/dex/tls.key + + # Configuration for telemetry + telemetry: + http: 0.0.0.0:5558 + +# Uncomment this block to enable configuration for the expiration time durations. +expiry: + signingKeys: "3h" + idTokens: "3h" + + # Options for controlling the logger. + logger: + level: "debug" + format: "text" # can also be "json" + +# Default values shown below +oauth2: + # use ["code", "token", "id_token"] to enable implicit flow for web-only clients + responseTypes: [ "code", "token", "id_token" ] # also allowed are "token" and "id_token" + # By default, Dex will ask for approval to share data with application + # (approval for sharing data from connected IdP to Dex is separate process on IdP) + skipApprovalScreen: false + # If only one authentication method is enabled, the default behavior is to + # go directly to it. For connected IdPs, this redirects the browser away + # from application to upstream provider such as the Google login page + alwaysShowLoginScreen: false + # Uncommend the passwordConnector to use a specific connector for password grants + passwordConnector: local + +# Instead of reading from an external storage, use this list of clients. +# +# If this option isn't chosen clients may be added through the gRPC API. +staticClients: + - id: example-app + redirectURIs: + - 'http://localhost:8080/oauth2/callback' + name: 'Example App' + secret: ZXhhbXBsZS1hcHAtc2VjcmV0 + +connectors: + - type: mockCallback + id: mock + name: Example + +# Let dex keep a list of passwords which can be used to login to dex. +enablePasswordDB: true + +# A static list of passwords to login the end user. By identifying here, dex +# won't look in its underlying storage for passwords. +# +# If this option isn't chosen users may be added through the gRPC API. +staticPasswords: + - email: "admin@example.com" + # bcrypt hash of the string "password" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/docs/sts/keycloak.md b/docs/sts/keycloak.md index 52fd6e534..7f8292066 100644 --- a/docs/sts/keycloak.md +++ b/docs/sts/keycloak.md @@ -34,7 +34,7 @@ Configure and install keycloak server by following [Keycloak Installation Guide] - `Claim JSON Type` is `string` - Save -- Open http://localhost:8080/auth/realms/demo/.well-known/openid-configuration to verify OpenID discovery document, verify it has `authorization_endpoint` and `jwks_uri` +- Open http://localhost:8080/auth/realms/minio/.well-known/openid-configuration to verify OpenID discovery document, verify it has `authorization_endpoint` and `jwks_uri` ### Configure MinIO ``` @@ -77,7 +77,7 @@ MINIO_IDENTITY_OPENID_COMMENT (sentence) optionally add a comment to this Set `identity_openid` config with `config_url`, `client_id` and restart MinIO ``` -~ mc admin config set myminio identity_openid config_url="http://localhost:8080/auth/realms/demo/.well-known/openid-configuration" client_id="account" +~ mc admin config set myminio identity_openid config_url="http://localhost:8080/auth/realms/minio/.well-known/openid-configuration" client_id="account" ``` > NOTE: You can configure the `scopes` parameter to restrict the OpenID scopes requested by minio to the IdP, for example, `"openid,policy_role_attribute"`, being `policy_role_attribute` a client_scope / client_mapper that maps a role attribute called policy to a `policy` claim returned by Keycloak @@ -87,10 +87,10 @@ mc admin service restart myminio ``` ### Using WebIdentiy API -Client ID can be found by clicking any of the clients listed [here](http://localhost:8080/auth/admin/master/console/#/realms/demo/clients). If you have followed the above steps docs, the default Client ID will be `account`. +Client ID can be found by clicking any of the clients listed [here](http://localhost:8080/auth/admin/master/console/#/realms/minio/clients). If you have followed the above steps docs, the default Client ID will be `account`. ``` -$ go run docs/sts/web-identity.go -cid account -csec 072e7f00-4289-469c-9ab2-bbe843c7f5a8 -config-ep "http://localhost:8080/auth/realms/demo/.well-known/openid-configuration" -port 8888 +$ go run docs/sts/web-identity.go -cid account -csec 072e7f00-4289-469c-9ab2-bbe843c7f5a8 -config-ep "http://localhost:8080/auth/realms/minio/.well-known/openid-configuration" -port 8888 2018/12/26 17:49:36 listening on http://localhost:8888/ ``` diff --git a/docs/sts/web-identity.go b/docs/sts/web-identity.go index 4214515fb..c3b77de7c 100644 --- a/docs/sts/web-identity.go +++ b/docs/sts/web-identity.go @@ -1,7 +1,7 @@ // +build ignore /* - * MinIO Cloud Storage, (C) 2019 MinIO, Inc. + * MinIO Cloud Storage, (C) 2019,2020 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,7 +85,7 @@ var ( ) // DiscoveryDoc - parses the output from openid-configuration -// for example https://accounts.google.com/.well-known/openid-configuration +// for example http://localhost:8080/auth/realms/minio/.well-known/openid-configuration type DiscoveryDoc struct { Issuer string `json:"issuer,omitempty"` AuthEndpoint string `json:"authorization_endpoint,omitempty"` @@ -129,7 +129,7 @@ func parseDiscoveryDoc(ustr string) (DiscoveryDoc, error) { func init() { flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint") flag.StringVar(&configEndpoint, "config-ep", - "https://accounts.google.com/.well-known/openid-configuration", + "http://localhost:8080/auth/realms/minio/.well-known/openid-configuration", "OpenID discovery document endpoint") flag.StringVar(&clientID, "cid", "", "Client ID") flag.StringVar(&clientSec, "csec", "", "Client Secret") @@ -151,9 +151,9 @@ func main() { return } - scopes := ddoc.ScopesSupported + scopes := ddoc.ScopesSupported if clientScopes != "" { - scopes = strings.Split(clientScopes, ","); + scopes = strings.Split(clientScopes, ",") } ctx := context.Background() diff --git a/docs/sts/web-identity.py b/docs/sts/web-identity.py new file mode 100644 index 000000000..084c5e86a --- /dev/null +++ b/docs/sts/web-identity.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import urllib +from uuid import uuid4 + +import boto3 +import requests +from botocore.client import Config +from flask import Flask, request + +boto3.set_stream_logger('boto3.resources', logging.DEBUG) + +authorize_url = "http://localhost:8080/auth/realms/minio/protocol/openid-connect/auth" +token_url = "http://localhost:8080/auth/realms/minio/protocol/openid-connect/token" + +# callback url specified when the application was defined +callback_uri = "http://localhost:8000/oauth2/callback" + +# keycloak id and secret +client_id = 'account' +client_secret = 'daaa3008-80f0-40f7-80d7-e15167531ff0' + +sts_client = boto3.client( + 'sts', + region_name='us-east-1', + use_ssl=False, + endpoint_url='http://localhost:9000', +) + +app = Flask(__name__) + + +@app.route('/') +def homepage(): + text = 'Authenticate with keycloak' + return text % make_authorization_url() + + +def make_authorization_url(): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + params = {"client_id": client_id, + "response_type": "code", + "state": state, + "redirect_uri": callback_uri, + "scope": "openid"} + + url = authorize_url + "?" + urllib.parse.urlencode(params) + return url + + +@app.route('/oauth2/callback') +def callback(): + error = request.args.get('error', '') + if error: + return "Error: " + error + + authorization_code = request.args.get('code') + + data = {'grant_type': 'authorization_code', + 'code': authorization_code, 'redirect_uri': callback_uri} + access_token_response = requests.post( + token_url, data=data, verify=False, allow_redirects=False, auth=(client_id, client_secret)) + + print('body: ' + access_token_response.text) + + # we can now use the access_token as much as we want to access protected resources. + tokens = json.loads(access_token_response.text) + access_token = tokens['access_token'] + + response = sts_client.assume_role_with_web_identity( + RoleArn='arn:aws:iam::123456789012:user/svc-internal-api', + RoleSessionName='test', + WebIdentityToken=access_token, + DurationSeconds=3600 + ) + + s3_resource = boto3.resource('s3', + endpoint_url='http://localhost:9000', + aws_access_key_id=response['Credentials']['AccessKeyId'], + aws_secret_access_key=response['Credentials']['SecretAccessKey'], + aws_session_token=response['Credentials']['SessionToken'], + config=Config(signature_version='s3v4'), + region_name='us-east-1') + + bucket = s3_resource.Bucket('testbucket') + + for obj in bucket.objects.all(): + print(obj) + + return "success" + + +if __name__ == '__main__': + app.run(debug=True, port=8000)