Repository: getmoto/moto

Issue:
NotImplementedError: Open ID Connect Providers in IAM
It looks like support for Open ID Connect providers in moto IAM hasn't come up yet. 

So I'm opening a feature request here as per https://github.com/spulec/moto/blob/master/CONTRIBUTING.md

```python
   iam_resource.create_open_id_connect_provider(
        Url='example.okta.com',
        ClientIDList=[
            'test_client_id_1',
            'test_client_id_2',
        ],
        ThumbprintList=[
            '988881adc9fc3655077dc2d4d757d480b5ea0e11',
        ]
    )
```

I'm using moto `1.3.8`.



Discussion:
Thanks; setting as a feature request.

Resolving patch:
diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py
--- a/moto/iam/exceptions.py
+++ b/moto/iam/exceptions.py
@@ -93,3 +93,27 @@ def __init__(self, tags, param='tags'):
         super(TooManyTags, self).__init__(
             'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy "
                                "constraint: Member must have length less than or equal to 50.".format(tags, param))
+
+
+class EntityAlreadyExists(RESTError):
+    code = 409
+
+    def __init__(self):
+        super(EntityAlreadyExists, self).__init__(
+            'EntityAlreadyExists', "Unknown")
+
+
+class ValidationError(RESTError):
+    code = 400
+
+    def __init__(self, message):
+        super(ValidationError, self).__init__(
+            'ValidationError', message)
+
+
+class InvalidInput(RESTError):
+    code = 400
+
+    def __init__(self, message):
+        super(InvalidInput, self).__init__(
+            'InvalidInput', message)
diff --git a/moto/iam/models.py b/moto/iam/models.py
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -7,6 +7,7 @@
 
 from cryptography import x509
 from cryptography.hazmat.backends import default_backend
+from six.moves.urllib.parse import urlparse
 
 from moto.core.exceptions import RESTError
 from moto.core import BaseBackend, BaseModel
@@ -14,8 +15,9 @@
 from moto.iam.policy_validation import IAMPolicyDocumentValidator
 
 from .aws_managed_policies import aws_managed_policies_data
-from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, IAMLimitExceededException, \
-    MalformedCertificate, DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig
+from .exceptions import (IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, IAMLimitExceededException,
+                         MalformedCertificate, DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig,
+                         EntityAlreadyExists, ValidationError, InvalidInput)
 from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
 
 ACCOUNT_ID = 123456789012
@@ -93,6 +95,82 @@ def arn(self):
         return "arn:aws:iam::{0}:saml-provider/{1}".format(ACCOUNT_ID, self.name)
 
 
+class OpenIDConnectProvider(BaseModel):
+    def __init__(self, url, thumbprint_list, client_id_list=None):
+        self._errors = []
+        self._validate(url, thumbprint_list, client_id_list)
+
+        parsed_url = urlparse(url)
+        self.url = parsed_url.netloc + parsed_url.path
+        self.thumbprint_list = thumbprint_list
+        self.client_id_list = client_id_list
+        self.create_date = datetime.utcnow()
+
+    @property
+    def arn(self):
+        return 'arn:aws:iam::{0}:oidc-provider/{1}'.format(ACCOUNT_ID, self.url)
+
+    @property
+    def created_iso_8601(self):
+        return iso_8601_datetime_without_milliseconds(self.create_date)
+
+    def _validate(self, url, thumbprint_list, client_id_list):
+        if any(len(client_id) > 255 for client_id in client_id_list):
+            self._errors.append(self._format_error(
+                key='clientIDList',
+                value=client_id_list,
+                constraint='Member must satisfy constraint: '
+                           '[Member must have length less than or equal to 255, '
+                           'Member must have length greater than or equal to 1]',
+            ))
+
+        if any(len(thumbprint) > 40 for thumbprint in thumbprint_list):
+            self._errors.append(self._format_error(
+                key='thumbprintList',
+                value=thumbprint_list,
+                constraint='Member must satisfy constraint: '
+                           '[Member must have length less than or equal to 40, '
+                           'Member must have length greater than or equal to 40]',
+            ))
+
+        if len(url) > 255:
+            self._errors.append(self._format_error(
+                key='url',
+                value=url,
+                constraint='Member must have length less than or equal to 255',
+            ))
+
+        self._raise_errors()
+
+        parsed_url = urlparse(url)
+        if not parsed_url.scheme or not parsed_url.netloc:
+            raise ValidationError('Invalid Open ID Connect Provider URL')
+
+        if len(thumbprint_list) > 5:
+            raise InvalidInput('Thumbprint list must contain fewer than 5 entries.')
+
+        if len(client_id_list) > 100:
+            raise IAMLimitExceededException('Cannot exceed quota for ClientIdsPerOpenIdConnectProvider: 100')
+
+    def _format_error(self, key, value, constraint):
+        return 'Value "{value}" at "{key}" failed to satisfy constraint: {constraint}'.format(
+            constraint=constraint,
+            key=key,
+            value=value,
+        )
+
+    def _raise_errors(self):
+        if self._errors:
+            count = len(self._errors)
+            plural = "s" if len(self._errors) > 1 else ""
+            errors = "; ".join(self._errors)
+            self._errors = []  # reset collected errors
+
+            raise ValidationError('{count} validation error{plural} detected: {errors}'.format(
+                count=count, plural=plural, errors=errors,
+            ))
+
+
 class PolicyVersion(object):
 
     def __init__(self,
@@ -515,6 +593,7 @@ def __init__(self):
         self.managed_policies = self._init_managed_policies()
         self.account_aliases = []
         self.saml_providers = {}
+        self.open_id_providers = {}
         self.policy_arn_regex = re.compile(
             r'^arn:aws:iam::[0-9]*:policy/.*$')
         super(IAMBackend, self).__init__()
@@ -1264,5 +1343,28 @@ def get_user_from_access_key_id(self, access_key_id):
                     return user
         return None
 
+    def create_open_id_connect_provider(self, url, thumbprint_list, client_id_list):
+        open_id_provider = OpenIDConnectProvider(url, thumbprint_list, client_id_list)
+
+        if open_id_provider.arn in self.open_id_providers:
+            raise EntityAlreadyExists
+
+        self.open_id_providers[open_id_provider.arn] = open_id_provider
+        return open_id_provider
+
+    def delete_open_id_connect_provider(self, arn):
+        self.open_id_providers.pop(arn, None)
+
+    def get_open_id_connect_provider(self, arn):
+        open_id_provider = self.open_id_providers.get(arn)
+
+        if not open_id_provider:
+            raise IAMNotFoundException('OpenIDConnect Provider not found for arn {}'.format(arn))
+
+        return open_id_provider
+
+    def list_open_id_connect_providers(self):
+        return list(self.open_id_providers.keys())
+
 
 iam_backend = IAMBackend()
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -755,6 +755,38 @@ def untag_role(self):
         template = self.response_template(UNTAG_ROLE_TEMPLATE)
         return template.render()
 
+    def create_open_id_connect_provider(self):
+        open_id_provider_url = self._get_param('Url')
+        thumbprint_list = self._get_multi_param('ThumbprintList.member')
+        client_id_list = self._get_multi_param('ClientIDList.member')
+
+        open_id_provider = iam_backend.create_open_id_connect_provider(open_id_provider_url, thumbprint_list, client_id_list)
+
+        template = self.response_template(CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE)
+        return template.render(open_id_provider=open_id_provider)
+
+    def delete_open_id_connect_provider(self):
+        open_id_provider_arn = self._get_param('OpenIDConnectProviderArn')
+
+        iam_backend.delete_open_id_connect_provider(open_id_provider_arn)
+
+        template = self.response_template(DELETE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE)
+        return template.render()
+
+    def get_open_id_connect_provider(self):
+        open_id_provider_arn = self._get_param('OpenIDConnectProviderArn')
+
+        open_id_provider = iam_backend.get_open_id_connect_provider(open_id_provider_arn)
+
+        template = self.response_template(GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE)
+        return template.render(open_id_provider=open_id_provider)
+
+    def list_open_id_connect_providers(self):
+        open_id_provider_arns = iam_backend.list_open_id_connect_providers()
+
+        template = self.response_template(LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE)
+        return template.render(open_id_provider_arns=open_id_provider_arns)
+
 
 LIST_ENTITIES_FOR_POLICY_TEMPLATE = """<ListEntitiesForPolicyResponse>
  <ListEntitiesForPolicyResult>
@@ -1974,3 +2006,57 @@ def untag_role(self):
     <RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
   </ResponseMetadata>
 </UntagRoleResponse>"""
+
+
+CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """<CreateOpenIDConnectProviderResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
+  <CreateOpenIDConnectProviderResult>
+    <OpenIDConnectProviderArn>{{ open_id_provider.arn }}</OpenIDConnectProviderArn>
+  </CreateOpenIDConnectProviderResult>
+  <ResponseMetadata>
+    <RequestId>f248366a-4f64-11e4-aefa-bfd6aEXAMPLE</RequestId>
+  </ResponseMetadata>
+</CreateOpenIDConnectProviderResponse>"""
+
+
+DELETE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """<DeleteOpenIDConnectProviderResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
+  <ResponseMetadata>
+    <RequestId>b5e49e29-4f64-11e4-aefa-bfd6aEXAMPLE</RequestId>
+  </ResponseMetadata>
+</DeleteOpenIDConnectProviderResponse>"""
+
+
+GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """<GetOpenIDConnectProviderResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
+  <GetOpenIDConnectProviderResult>
+    <ThumbprintList>
+      {% for thumbprint in open_id_provider.thumbprint_list %}
+      <member>{{ thumbprint }}</member>
+      {% endfor %}
+    </ThumbprintList>
+    <CreateDate>{{ open_id_provider.created_iso_8601 }}</CreateDate>
+    <ClientIDList>
+      {% for client_id in open_id_provider.client_id_list %}
+      <member>{{ client_id }}</member>
+      {% endfor %}
+    </ClientIDList>
+    <Url>{{ open_id_provider.url }}</Url>
+  </GetOpenIDConnectProviderResult>
+  <ResponseMetadata>
+    <RequestId>2c91531b-4f65-11e4-aefa-bfd6aEXAMPLE</RequestId>
+  </ResponseMetadata>
+</GetOpenIDConnectProviderResponse>"""
+
+
+LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE = """<ListOpenIDConnectProvidersResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
+  <ListOpenIDConnectProvidersResult>
+    <OpenIDConnectProviderList>
+      {% for open_id_provider_arn in open_id_provider_arns %}
+      <member>
+        <Arn>{{ open_id_provider_arn }}</Arn>
+      </member>
+      {% endfor %}
+    </OpenIDConnectProviderList>
+  </ListOpenIDConnectProvidersResult>
+  <ResponseMetadata>
+    <RequestId>de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE</RequestId>
+  </ResponseMetadata>
+</ListOpenIDConnectProvidersResponse>"""


Test patch:
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -9,6 +9,8 @@
 import sys
 from boto.exception import BotoServerError
 from botocore.exceptions import ClientError
+from dateutil.tz import tzutc
+
 from moto import mock_iam, mock_iam_deprecated
 from moto.iam.models import aws_managed_policies
 from nose.tools import assert_raises, assert_equals
@@ -1565,3 +1567,251 @@ def test_create_role_with_permissions_boundary():
 
     # Ensure the PermissionsBoundary is included in role listing as well
     conn.list_roles().get('Roles')[0].get('PermissionsBoundary').should.equal(expected)
+
+
+@mock_iam
+def test_create_open_id_connect_provider():
+    client = boto3.client('iam', region_name='us-east-1')
+    response = client.create_open_id_connect_provider(
+        Url='https://example.com',
+        ThumbprintList=[]  # even it is required to provide at least one thumbprint, AWS accepts an empty list
+    )
+
+    response['OpenIDConnectProviderArn'].should.equal(
+        'arn:aws:iam::123456789012:oidc-provider/example.com'
+    )
+
+    response = client.create_open_id_connect_provider(
+        Url='http://example.org',
+        ThumbprintList=[
+            'b' * 40
+        ],
+        ClientIDList=[
+            'b'
+        ]
+    )
+
+    response['OpenIDConnectProviderArn'].should.equal(
+        'arn:aws:iam::123456789012:oidc-provider/example.org'
+    )
+
+    response = client.create_open_id_connect_provider(
+        Url='http://example.org/oidc',
+        ThumbprintList=[]
+    )
+
+    response['OpenIDConnectProviderArn'].should.equal(
+        'arn:aws:iam::123456789012:oidc-provider/example.org/oidc'
+    )
+
+    response = client.create_open_id_connect_provider(
+        Url='http://example.org/oidc-query?test=true',
+        ThumbprintList=[]
+    )
+
+    response['OpenIDConnectProviderArn'].should.equal(
+        'arn:aws:iam::123456789012:oidc-provider/example.org/oidc-query'
+    )
+
+
+@mock_iam
+def test_create_open_id_connect_provider_errors():
+    client = boto3.client('iam', region_name='us-east-1')
+    client.create_open_id_connect_provider(
+        Url='https://example.com',
+        ThumbprintList=[]
+    )
+
+    client.create_open_id_connect_provider.when.called_with(
+        Url='https://example.com',
+        ThumbprintList=[]
+    ).should.throw(
+        ClientError,
+        'Unknown'
+    )
+
+    client.create_open_id_connect_provider.when.called_with(
+        Url='example.org',
+        ThumbprintList=[]
+    ).should.throw(
+        ClientError,
+        'Invalid Open ID Connect Provider URL'
+    )
+
+    client.create_open_id_connect_provider.when.called_with(
+        Url='example',
+        ThumbprintList=[]
+    ).should.throw(
+        ClientError,
+        'Invalid Open ID Connect Provider URL'
+    )
+
+    client.create_open_id_connect_provider.when.called_with(
+        Url='http://example.org',
+        ThumbprintList=[
+            'a' * 40,
+            'b' * 40,
+            'c' * 40,
+            'd' * 40,
+            'e' * 40,
+            'f' * 40,
+        ]
+    ).should.throw(
+        ClientError,
+        'Thumbprint list must contain fewer than 5 entries.'
+    )
+
+    too_many_client_ids = ['{}'.format(i) for i in range(101)]
+    client.create_open_id_connect_provider.when.called_with(
+        Url='http://example.org',
+        ThumbprintList=[],
+        ClientIDList=too_many_client_ids
+    ).should.throw(
+        ClientError,
+        'Cannot exceed quota for ClientIdsPerOpenIdConnectProvider: 100'
+    )
+
+    too_long_url = 'b' * 256
+    too_long_thumbprint = 'b' * 41
+    too_long_client_id = 'b' * 256
+    client.create_open_id_connect_provider.when.called_with(
+        Url=too_long_url,
+        ThumbprintList=[
+            too_long_thumbprint
+        ],
+        ClientIDList=[
+            too_long_client_id
+        ]
+    ).should.throw(
+        ClientError,
+        '3 validation errors detected: '
+        'Value "{0}" at "clientIDList" failed to satisfy constraint: '
+        'Member must satisfy constraint: '
+        '[Member must have length less than or equal to 255, '
+        'Member must have length greater than or equal to 1]; '
+        'Value "{1}" at "thumbprintList" failed to satisfy constraint: '
+        'Member must satisfy constraint: '
+        '[Member must have length less than or equal to 40, '
+        'Member must have length greater than or equal to 40]; '
+        'Value "{2}" at "url" failed to satisfy constraint: '
+        'Member must have length less than or equal to 255'.format([too_long_client_id], [too_long_thumbprint], too_long_url)
+    )
+
+
+@mock_iam
+def test_delete_open_id_connect_provider():
+    client = boto3.client('iam', region_name='us-east-1')
+    response = client.create_open_id_connect_provider(
+        Url='https://example.com',
+        ThumbprintList=[]
+    )
+    open_id_arn = response['OpenIDConnectProviderArn']
+
+    client.delete_open_id_connect_provider(
+        OpenIDConnectProviderArn=open_id_arn
+    )
+
+    client.get_open_id_connect_provider.when.called_with(
+        OpenIDConnectProviderArn=open_id_arn
+    ).should.throw(
+        ClientError,
+        'OpenIDConnect Provider not found for arn {}'.format(open_id_arn)
+    )
+
+    # deleting a non existing provider should be successful
+    client.delete_open_id_connect_provider(
+        OpenIDConnectProviderArn=open_id_arn
+    )
+
+
+@mock_iam
+def test_get_open_id_connect_provider():
+    client = boto3.client('iam', region_name='us-east-1')
+    response = client.create_open_id_connect_provider(
+        Url='https://example.com',
+        ThumbprintList=[
+            'b' * 40
+        ],
+        ClientIDList=[
+            'b'
+        ]
+    )
+    open_id_arn = response['OpenIDConnectProviderArn']
+
+    response = client.get_open_id_connect_provider(
+        OpenIDConnectProviderArn=open_id_arn
+    )
+
+    response['Url'].should.equal('example.com')
+    response['ThumbprintList'].should.equal([
+        'b' * 40
+    ])
+    response['ClientIDList'].should.equal([
+        'b'
+    ])
+    response.should.have.key('CreateDate').should.be.a(datetime)
+
+
+@mock_iam
+def test_get_open_id_connect_provider_errors():
+    client = boto3.client('iam', region_name='us-east-1')
+    response = client.create_open_id_connect_provider(
+        Url='https://example.com',
+        ThumbprintList=[
+            'b' * 40
+        ],
+        ClientIDList=[
+            'b'
+        ]
+    )
+    open_id_arn = response['OpenIDConnectProviderArn']
+
+    client.get_open_id_connect_provider.when.called_with(
+        OpenIDConnectProviderArn=open_id_arn + '-not-existing'
+    ).should.throw(
+        ClientError,
+        'OpenIDConnect Provider not found for arn {}'.format(open_id_arn + '-not-existing')
+    )
+
+
+@mock_iam
+def test_list_open_id_connect_providers():
+    client = boto3.client('iam', region_name='us-east-1')
+    response = client.create_open_id_connect_provider(
+        Url='https://example.com',
+        ThumbprintList=[]
+    )
+    open_id_arn_1 = response['OpenIDConnectProviderArn']
+
+    response = client.create_open_id_connect_provider(
+        Url='http://example.org',
+        ThumbprintList=[
+            'b' * 40
+        ],
+        ClientIDList=[
+            'b'
+        ]
+    )
+    open_id_arn_2 = response['OpenIDConnectProviderArn']
+
+    response = client.create_open_id_connect_provider(
+        Url='http://example.org/oidc',
+        ThumbprintList=[]
+    )
+    open_id_arn_3 = response['OpenIDConnectProviderArn']
+
+    response = client.list_open_id_connect_providers()
+
+    sorted(response['OpenIDConnectProviderList'], key=lambda i: i['Arn']).should.equal(
+        [
+            {
+                'Arn': open_id_arn_1
+            },
+            {
+                'Arn': open_id_arn_2
+            },
+            {
+                'Arn': open_id_arn_3
+            }
+        ]
+    )