JudgeProphet
JudgeProphet

Reputation: 1729

How to connect to Microsoft Azure Key Vault get a token and read a value from the Vault with Ruby On Rails

My Application is built using Ruby On Rails. I'm ask to use Microsoft Azure Key Vault to store our secrets string.

I know there is Gems available made By the Microsoft Teams:

https://rubygems.org/gems/azure_mgmt_key_vault

https://rubygems.org/gems/azure_key_vault

How do I "extract" or "reference" a Key and pass it to my application?

Upvotes: 3

Views: 1539

Answers (1)

JudgeProphet
JudgeProphet

Reputation: 1729

With a lot of Work and Sweat I figured out. Even more, I was able to use a Certificate to connect to Microsoft Azure Key Vault. So below I put all my code with 2 ways to get the token. One with a client secret id and the other with a certificate.

I found how to generate a self-sign certificate (for debuging purpose) and get the encode thumbprint:

Certificat that was upload to Azure was generated with: openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_certificate.pem -nodes -days 3650

To obtain the x5t encode base64 thumbprint of the certificate: echo $(openssl x509 -in public_certificate.pem -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64

I built a GEM.

I have a Configuration file lib\azurekeyvault\configuration.rb:

module AzureKeyVault
    class Configuration
      attr_accessor :azure_tenant_id, :azure_client_id, :azure_client_secret, :azure_subscription_id, :vault_base_url, :api_version, :resource, :azure_certificate_thumbprint, :azure_certificate_private_key_file
  
      def initialize

        @azure_tenant_id = nil
        @azure_client_id = nil
        @azure_client_secret = nil
        @azure_subscription_id = nil
        @vault_base_url = nil
        @api_version = nil
        @resource = nil
        @azure_certificate_thumbprint = nil
        @azure_certificate_private_key_file = nil        

      end
    end
  end

This is the file where the magic happen lib\azurekeyvault\extraction.rb:

module AzureKeyVault
    require 'singleton'

    class Extraction
        include Singleton

        def initialize
            @configuration = AzureKeyVault.configuration
        end

        def get_value(secret_name, secret_version = nil)
            get_secret(secret_name, secret_version)
        end

        private
        ### Get a Secret value from Microsoft Azure Vault
        ## secret_name: Name of the Key which contain the value
        ## secret_version (optional): Version of the key value we need, by omitting version the system to use the latest available version
        def get_secret(secret_name, secret_version = nil)
            # GET {vaultBaseUrl}/secrets/{secret-name}/{secret-version}?api-version=7.1
            vault_base_url  = @configuration.vault_base_url
            api_version     = @configuration.api_version
            azure_certificate_thumbprint = @configuration.azure_certificate_thumbprint

            auth_token = nil
            if azure_certificate_thumbprint.nil?
                auth_token = get_auth_token()
            else
                auth_token = get_auth_certificate_token()
            end
            return nil if auth_token.nil?

            url = "#{vault_base_url}/secrets/#{secret_name}/#{secret_version}?api-version=#{api_version}"
            headers = { 'Authorization' => "Bearer " + auth_token }

            begin
                response = HTTParty.get(url, {headers: headers})
                return response.parsed_response['value']
            rescue HTTParty::Error => e
                puts "HTTParty ERROR: #{e.message}"
                raise e
            rescue Exception => e
                puts "ERROR: #{e.message}"
                raise e               
            end
        end
        
        def get_auth_token
            #Microsoft identity platform and the OAuth 2.0 client credentials flow
            # https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
            # https://learn.microsoft.com/en-us/azure/active-directory/azuread-dev/v1-oauth2-client-creds-grant-flow#request-an-access-token
            
            azure_tenant_id = @configuration.azure_tenant_id
            azure_client_id = @configuration.azure_client_id
            azure_client_secret = @configuration.azure_client_secret
            resource = @configuration.resource

            authUrl = "https://login.microsoftonline.com/#{azure_tenant_id}/oauth2/token"

            data = {
                'grant_type': 'client_credentials',
                'client_id': azure_client_id,
                'client_secret': azure_client_secret,
                'resource': resource
            }

            begin

                response= HTTParty.post(authUrl, body: data)
                token = nil

                if response
                    #puts response.to_json
                    token = response.parsed_response['access_token']
                end
                return token
            rescue HTTParty::Error => e
                puts "HTTParty ERROR: #{e.message}"
                raise e
            rescue Exception => e
                puts "ERROR: #{e.message}"
                raise e               
            end
        end
        def get_auth_certificate_token

            begin
                # Microsoft identity platform and the OAuth 2.0 client credentials flow
                #
                # Certificat that was upload to Azure was generated with: 
                # openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_certificate.pem -nodes -days 3650
                #
                # To obtain the x5t encode base64 thumbprint of the certificate: 
                # echo $(openssl x509 -in public_certificate.pem -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64
        
                # https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
                # https://learn.microsoft.com/en-us/azure/active-directory/azuread-dev/v1-oauth2-client-creds-grant-flow#request-an-access-token
                
                azure_tenant_id = @configuration.azure_tenant_id
                azure_client_id = @configuration.azure_client_id
                resource        = @configuration.resource
                azure_certificate_thumbprint        = @configuration.azure_certificate_thumbprint
                azure_certificate_private_key_file  = @configuration.azure_certificate_private_key_file

                authUrl = "https://login.microsoftonline.com/#{azure_tenant_id}/oauth2/token"
                exp = Time.now.to_i + 4 * 3600
                nbf = Time.now.to_i - 3600
                jti = SecureRandom.uuid

                #//x5t THUMBPRINT of Cert
                header = {
                    "alg": "RS256",
                    "typ": "JWT",
                    "x5t": azure_certificate_thumbprint
                }
                #Claim (payload)
                payload = {
                    "aud": authUrl,
                    "exp": exp,
                    "iss": azure_client_id,
                    "jti": jti,
                    "nbf": nbf,
                    "sub": azure_client_id
                }
                            
                token = "#{Base64.strict_encode64(header.to_json)}.#{Base64.strict_encode64(payload.to_json)}"

                # Get the private key, from the file
                azure_certificate_private_key = OpenSSL::PKey.read(File.read(azure_certificate_private_key_file))
                # The hash algorithm, I assume SHA256 is being used
                base64_signature = Base64.strict_encode64(azure_certificate_private_key.sign(OpenSSL::Digest::SHA256.new, token))

                jwt_client_assertion = "#{token}.#{base64_signature}"

                data = {
                    'grant_type': 'client_credentials',
                    'client_id': azure_client_id,
                    'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
                    'client_assertion': jwt_client_assertion,
                    'resource': resource
                }

                response = HTTParty.post(authUrl, body: data)
                token = nil

                if response
                    token = response.parsed_response['access_token']
                end
                return token
            rescue HTTParty::Error => e
                puts "HTTParty ERROR: #{e.message}"
                raise e
            rescue Exception => e
                puts "ERROR: #{e.message}"
                raise e               
            end
        end        
    end
end

I have also an Initialiser where I assign value for my configuration variables

AzureKeyVault.configure do |config|

    config.azure_tenant_id = ENV["AZURE_VAULT_TENANT_ID"]
    config.azure_client_id = ENV["AZURE_VAULT_CLIENT_ID"]
    config.azure_client_secret = ENV["AZURE_VAULT_CLIENT_SECRET"]
    config.azure_subscription_id = ENV["AZURE_VAULT_SUBSCRIPTION_ID"]
    config.vault_base_url = ENV["AZURE_VAULT_BASE_URL"]
    config.api_version = ENV["AZURE_VAULT_API_VERSION"]
    config.resource = ENV["AZURE_VAULT_RESOURCE"]
    # To obtain the x5t encode base64 thumbprint of the certificate: 
    # echo $(openssl x509 -in public_certificate.pem -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64
    config.azure_certificate_thumbprint = ENV["AZURE_CERTIFICATE_THUMBPRINT"]
    #Certificat that was upload to Azure was generated with: 
    # openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_certificate.pem -nodes    
    config.azure_certificate_private_key_file = ENV["AZURE_CERTIFICATE_PRIVATE_KEY_FILE"]

end

Note: This post and answer (@Jason Johnston) help me a lot to understand what was going on: Office 365 Rest API - Daemon week authentication

Upvotes: 8

Related Questions