From 9b9ef0f9e86258532ce65ef43051f439102c140a Mon Sep 17 00:00:00 2001 From: Denis CLAVIER Date: Thu, 30 Apr 2020 15:43:07 +0200 Subject: [PATCH 1/4] Update Mattermost-LDAP to V2.0 --- Demo/config.json | 475 +++++++++++++ Demo/docker-compose.yaml | 65 ++ Demo/nginx.conf | 136 ++++ Docker/README.md | 123 ++-- Docker/mattermostldap/Dockerfile | 23 + Docker/oauth/Dockerfile | 31 - Docker/oauth/files/config_db.php | 14 - Docker/oauth/files/config_ldap.php | 16 - Docker/php-ldap-pgsql/Dockerfile | 12 + Docker/postgres/Dockerfile | 13 - LICENSE | 2 +- Puppet/mattermostldap/README.md | 39 +- README.md | 199 ++++-- changelog.md | 23 + config_init.sh.example | 16 - .../config_init.sh.example | 10 +- init_mysql.sh => db_init/init_mysql.sh | 30 +- .../files/init.sh => db_init/init_postgres.sh | 28 +- docker-compose.yaml | 47 ++ env.example | 85 +++ init_postgres.sh | 64 -- oauth/.htaccess | 10 + oauth/LDAP/LDAP.php | 257 +++---- oauth/LDAP/LDAPInterface.php | 54 +- oauth/LDAP/config_ldap.php.example | 20 +- oauth/OAuth2/.htaccess | 2 +- oauth/OAuth2/Autoloader.php | 12 +- .../ClientAssertionTypeInterface.php | 13 + .../OAuth2/ClientAssertionType/HttpBasic.php | 48 +- .../OAuth2/Controller/AuthorizeController.php | 140 +++- .../AuthorizeControllerInterface.php | 37 +- .../OAuth2/Controller/ResourceController.php | 50 +- .../ResourceControllerInterface.php | 29 +- oauth/OAuth2/Controller/TokenController.php | 74 +- .../Controller/TokenControllerInterface.php | 27 +- .../OAuth2/Encryption/EncryptionInterface.php | 34 + oauth/OAuth2/Encryption/FirebaseJwt.php | 47 ++ oauth/OAuth2/Encryption/Jwt.php | 223 ++++++ oauth/OAuth2/GrantType/AuthorizationCode.php | 48 +- oauth/OAuth2/GrantType/ClientCredentials.php | 98 +++ oauth/OAuth2/GrantType/GrantTypeInterface.php | 41 +- oauth/OAuth2/GrantType/JwtBearer.php | 247 +++++++ oauth/OAuth2/GrantType/RefreshToken.php | 63 +- oauth/OAuth2/GrantType/UserCredentials.php | 123 ++++ .../OpenID/Controller/AuthorizeController.php | 135 ++++ .../AuthorizeControllerInterface.php | 12 + .../OpenID/Controller/UserInfoController.php | 62 ++ .../UserInfoControllerInterface.php | 30 + .../OpenID/GrantType/AuthorizationCode.php | 41 ++ .../OpenID/ResponseType/AuthorizationCode.php | 66 ++ .../AuthorizationCodeInterface.php | 27 + .../OpenID/ResponseType/CodeIdToken.php | 40 ++ .../ResponseType/CodeIdTokenInterface.php | 9 + oauth/OAuth2/OpenID/ResponseType/IdToken.php | 178 +++++ .../OpenID/ResponseType/IdTokenInterface.php | 30 + .../OpenID/ResponseType/IdTokenToken.php | 45 ++ .../ResponseType/IdTokenTokenInterface.php | 9 + .../Storage/AuthorizationCodeInterface.php | 37 + .../OpenID/Storage/UserClaimsInterface.php | 35 + oauth/OAuth2/Request.php | 77 +- oauth/OAuth2/RequestInterface.php | 23 + oauth/OAuth2/Response.php | 134 +++- oauth/OAuth2/ResponseInterface.php | 31 +- oauth/OAuth2/ResponseType/AccessToken.php | 73 +- .../ResponseType/AccessTokenInterface.php | 11 +- .../OAuth2/ResponseType/AuthorizationCode.php | 7 +- .../AuthorizationCodeInterface.php | 12 +- oauth/OAuth2/ResponseType/JwtAccessToken.php | 159 +++++ .../ResponseType/ResponseTypeInterface.php | 5 + oauth/OAuth2/Scope.php | 41 +- oauth/OAuth2/ScopeInterface.php | 19 +- oauth/OAuth2/Server.php | 467 +++++++++++-- oauth/OAuth2/Storage/AccessTokenInterface.php | 49 +- .../Storage/AuthorizationCodeInterface.php | 65 +- oauth/OAuth2/Storage/Cassandra.php | 660 ++++++++++++++++++ oauth/OAuth2/Storage/CouchbaseDB.php | 2 +- oauth/OAuth2/Storage/DynamoDB.php | 540 ++++++++++++++ oauth/OAuth2/Storage/JwtAccessToken.php | 87 +++ .../Storage/JwtAccessTokenInterface.php | 14 + oauth/OAuth2/Storage/JwtBearerInterface.php | 74 ++ oauth/OAuth2/Storage/Memory.php | 381 ++++++++++ oauth/OAuth2/Storage/Mongo.php | 392 +++++++++++ oauth/OAuth2/Storage/MongoDB.php | 380 ++++++++++ oauth/OAuth2/Storage/Pdo.php | 518 ++++++++++++-- oauth/OAuth2/Storage/PublicKeyInterface.php | 30 + oauth/OAuth2/Storage/Redis.php | 321 +++++++++ .../Storage/UserCredentialsInterface.php | 52 ++ oauth/OAuth2/TokenType/Mac.php | 22 + oauth/authorize.php | 66 +- oauth/config_db.php.example | 16 +- oauth/connexion.php | 112 ++- oauth/resource.php | 21 +- oauth/style.css | 37 +- oauth/token.php | 2 +- 94 files changed, 7776 insertions(+), 1028 deletions(-) create mode 100644 Demo/config.json create mode 100644 Demo/docker-compose.yaml create mode 100644 Demo/nginx.conf create mode 100644 Docker/mattermostldap/Dockerfile delete mode 100644 Docker/oauth/Dockerfile delete mode 100644 Docker/oauth/files/config_db.php delete mode 100644 Docker/oauth/files/config_ldap.php create mode 100644 Docker/php-ldap-pgsql/Dockerfile delete mode 100644 Docker/postgres/Dockerfile create mode 100644 changelog.md delete mode 100755 config_init.sh.example rename Docker/postgres/files/config_init.sh => db_init/config_init.sh.example (63%) mode change 100644 => 100755 rename init_mysql.sh => db_init/init_mysql.sh (74%) rename Docker/postgres/files/init.sh => db_init/init_postgres.sh (75%) mode change 100644 => 100755 create mode 100644 docker-compose.yaml create mode 100644 env.example delete mode 100755 init_postgres.sh create mode 100644 oauth/OAuth2/Encryption/EncryptionInterface.php create mode 100644 oauth/OAuth2/Encryption/FirebaseJwt.php create mode 100644 oauth/OAuth2/Encryption/Jwt.php create mode 100644 oauth/OAuth2/GrantType/ClientCredentials.php create mode 100644 oauth/OAuth2/GrantType/JwtBearer.php create mode 100644 oauth/OAuth2/GrantType/UserCredentials.php create mode 100644 oauth/OAuth2/OpenID/Controller/AuthorizeController.php create mode 100644 oauth/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php create mode 100644 oauth/OAuth2/OpenID/Controller/UserInfoController.php create mode 100644 oauth/OAuth2/OpenID/Controller/UserInfoControllerInterface.php create mode 100644 oauth/OAuth2/OpenID/GrantType/AuthorizationCode.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/AuthorizationCode.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/CodeIdToken.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/IdToken.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/IdTokenInterface.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/IdTokenToken.php create mode 100644 oauth/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php create mode 100644 oauth/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php create mode 100644 oauth/OAuth2/OpenID/Storage/UserClaimsInterface.php create mode 100644 oauth/OAuth2/ResponseType/JwtAccessToken.php create mode 100644 oauth/OAuth2/Storage/Cassandra.php create mode 100644 oauth/OAuth2/Storage/DynamoDB.php create mode 100644 oauth/OAuth2/Storage/JwtAccessToken.php create mode 100644 oauth/OAuth2/Storage/JwtAccessTokenInterface.php create mode 100644 oauth/OAuth2/Storage/JwtBearerInterface.php create mode 100644 oauth/OAuth2/Storage/Memory.php create mode 100644 oauth/OAuth2/Storage/Mongo.php create mode 100644 oauth/OAuth2/Storage/MongoDB.php create mode 100644 oauth/OAuth2/Storage/PublicKeyInterface.php create mode 100644 oauth/OAuth2/Storage/Redis.php create mode 100644 oauth/OAuth2/Storage/UserCredentialsInterface.php create mode 100644 oauth/OAuth2/TokenType/Mac.php diff --git a/Demo/config.json b/Demo/config.json new file mode 100644 index 0000000..66e5edd --- /dev/null +++ b/Demo/config.json @@ -0,0 +1,475 @@ +{ + "ServiceSettings": { + "SiteURL": "http://localhost", + "WebsocketURL": "", + "LicenseFileLocation": "", + "ListenAddress": ":8065", + "ConnectionSecurity": "", + "TLSCertFile": "", + "TLSKeyFile": "", + "TLSMinVer": "1.2", + "TLSStrictTransport": false, + "TLSStrictTransportMaxAge": 63072000, + "TLSOverwriteCiphers": [], + "UseLetsEncrypt": false, + "LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache", + "Forward80To443": false, + "TrustedProxyIPHeader": [ + "X-Forwarded-For", + "X-Real-IP" + ], + "ReadTimeout": 300, + "WriteTimeout": 300, + "MaximumLoginAttempts": 10, + "GoroutineHealthThreshold": -1, + "GoogleDeveloperKey": "", + "EnableOAuthServiceProvider": false, + "EnableIncomingWebhooks": true, + "EnableOutgoingWebhooks": true, + "EnableCommands": true, + "EnableOnlyAdminIntegrations": true, + "EnablePostUsernameOverride": false, + "EnablePostIconOverride": false, + "EnableLinkPreviews": false, + "EnableTesting": false, + "EnableDeveloper": false, + "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, + "AllowedUntrustedInternalConnections": "", + "EnableMultifactorAuthentication": false, + "EnforceMultifactorAuthentication": false, + "EnableUserAccessTokens": false, + "AllowCorsFrom": "", + "CorsExposedHeaders": "", + "CorsAllowCredentials": false, + "CorsDebug": false, + "AllowCookiesForSubdomains": false, + "SessionLengthWebInDays": 30, + "SessionLengthMobileInDays": 30, + "SessionLengthSSOInDays": 30, + "SessionCacheInMinutes": 10, + "SessionIdleTimeoutInMinutes": 0, + "WebsocketSecurePort": 443, + "WebsocketPort": 80, + "WebserverMode": "gzip", + "EnableCustomEmoji": false, + "EnableEmojiPicker": true, + "EnableGifPicker": false, + "GfycatApiKey": "2_KtH_W5", + "GfycatApiSecret": "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof", + "RestrictCustomEmojiCreation": "all", + "RestrictPostDelete": "all", + "AllowEditPost": "always", + "PostEditTimeLimit": -1, + "TimeBetweenUserTypingUpdatesMilliseconds": 5000, + "EnablePostSearch": true, + "MinimumHashtagLength": 3, + "EnableUserTypingMessages": true, + "EnableChannelViewedMessages": true, + "EnableUserStatuses": true, + "ExperimentalEnableAuthenticationTransfer": true, + "ClusterLogTimeoutMilliseconds": 2000, + "CloseUnusedDirectMessages": false, + "EnablePreviewFeatures": true, + "EnableTutorial": true, + "ExperimentalEnableDefaultChannelLeaveJoinMessages": true, + "ExperimentalGroupUnreadChannels": "disabled", + "ExperimentalChannelOrganization": false, + "ImageProxyType": "", + "ImageProxyURL": "", + "ImageProxyOptions": "", + "EnableAPITeamDeletion": false, + "ExperimentalEnableHardenedMode": false, + "DisableLegacyMFA": false, + "ExperimentalStrictCSRFEnforcement": false, + "EnableEmailInvitations": false, + "DisableBotsWhenOwnerIsDeactivated": true, + "EnableBotAccountCreation": false, + "EnableSVGs": true, + "EnableLatex": true + }, + "TeamSettings": { + "SiteName": "Mattermost", + "MaxUsersPerTeam": 50, + "EnableTeamCreation": true, + "EnableUserCreation": true, + "EnableOpenServer": false, + "EnableUserDeactivation": false, + "RestrictCreationToDomains": "", + "EnableCustomBrand": false, + "CustomBrandText": "", + "CustomDescriptionText": "", + "RestrictDirectMessage": "any", + "RestrictTeamInvite": "all", + "RestrictPublicChannelManagement": "all", + "RestrictPrivateChannelManagement": "all", + "RestrictPublicChannelCreation": "all", + "RestrictPrivateChannelCreation": "all", + "RestrictPublicChannelDeletion": "all", + "RestrictPrivateChannelDeletion": "all", + "RestrictPrivateChannelManageMembers": "all", + "EnableXToLeaveChannelsFromLHS": false, + "UserStatusAwayTimeout": 300, + "MaxChannelsPerTeam": 2000, + "MaxNotificationsPerChannel": 1000000, + "EnableConfirmNotificationsToChannel": true, + "TeammateNameDisplay": "username", + "ExperimentalViewArchivedChannels": false, + "ExperimentalEnableAutomaticReplies": false, + "ExperimentalHideTownSquareinLHS": false, + "ExperimentalTownSquareIsReadOnly": false, + "LockTeammateNameDisplay": false, + "ExperimentalPrimaryTeam": "", + "ExperimentalDefaultChannels": [] + }, + "ClientRequirements": { + "AndroidLatestVersion": "", + "AndroidMinVersion": "", + "DesktopLatestVersion": "", + "DesktopMinVersion": "", + "IosLatestVersion": "", + "IosMinVersion": "" + }, + "SqlSettings": { + "DriverName": "mysql", + "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8", + "DataSourceReplicas": [], + "DataSourceSearchReplicas": [], + "MaxIdleConns": 20, + "ConnMaxLifetimeMilliseconds": 3600000, + "MaxOpenConns": 300, + "Trace": false, + "AtRestEncryptKey": "95ps7omhzmhusdfqh5bki5ye4xfd4hgw", + "QueryTimeout": 30 + }, + "LogSettings": { + "EnableConsole": true, + "ConsoleLevel": "DEBUG", + "ConsoleJson": true, + "EnableFile": true, + "FileLevel": "INFO", + "FileJson": true, + "FileLocation": "", + "EnableWebhookDebugging": true, + "EnableDiagnostics": true + }, + "NotificationLogSettings": { + "EnableConsole": true, + "ConsoleLevel": "DEBUG", + "ConsoleJson": true, + "EnableFile": true, + "FileLevel": "INFO", + "FileJson": true, + "FileLocation": "" + }, + "PasswordSettings": { + "MinimumLength": 5, + "Lowercase": false, + "Number": false, + "Uppercase": false, + "Symbol": false + }, + "FileSettings": { + "EnableFileAttachments": true, + "EnableMobileUpload": true, + "EnableMobileDownload": true, + "MaxFileSize": 52428800, + "DriverName": "local", + "Directory": "/mm/mattermost-data/", + "EnablePublicLink": false, + "PublicLinkSalt": "g3w9kzz9ewg1bskanhruqorygm81rp7j", + "InitialFont": "nunito-bold.ttf", + "AmazonS3AccessKeyId": "", + "AmazonS3SecretAccessKey": "", + "AmazonS3Bucket": "", + "AmazonS3Region": "", + "AmazonS3Endpoint": "s3.amazonaws.com", + "AmazonS3SSL": true, + "AmazonS3SignV2": false, + "AmazonS3SSE": false, + "AmazonS3Trace": false + }, + "EmailSettings": { + "EnableSignUpWithEmail": false, + "EnableSignInWithEmail": false, + "EnableSignInWithUsername": false, + "SendEmailNotifications": true, + "UseChannelInEmailNotifications": false, + "RequireEmailVerification": false, + "FeedbackName": "", + "FeedbackEmail": "test@example.com", + "ReplyToAddress": "test@example.com", + "FeedbackOrganization": "", + "EnableSMTPAuth": false, + "SMTPUsername": "", + "SMTPPassword": "", + "SMTPServer": "localhost", + "SMTPPort": "10025", + "ConnectionSecurity": "", + "SendPushNotifications": false, + "PushNotificationServer": "", + "PushNotificationContents": "generic", + "EnableEmailBatching": false, + "EmailBatchingBufferSize": 256, + "EmailBatchingInterval": 30, + "EnablePreviewModeBanner": true, + "SkipServerCertificateVerification": false, + "EmailNotificationContentsType": "full", + "LoginButtonColor": "", + "LoginButtonBorderColor": "", + "LoginButtonTextColor": "" + }, + "RateLimitSettings": { + "Enable": false, + "PerSec": 10, + "MaxBurst": 100, + "MemoryStoreSize": 10000, + "VaryByRemoteAddr": true, + "VaryByUser": false, + "VaryByHeader": "" + }, + "PrivacySettings": { + "ShowEmailAddress": true, + "ShowFullName": true + }, + "SupportSettings": { + "TermsOfServiceLink": "https://about.mattermost.com/default-terms/", + "PrivacyPolicyLink": "https://about.mattermost.com/default-privacy-policy/", + "AboutLink": "https://about.mattermost.com/default-about/", + "HelpLink": "https://about.mattermost.com/default-help/", + "ReportAProblemLink": "https://about.mattermost.com/default-report-a-problem/", + "SupportEmail": "feedback@mattermost.com", + "CustomTermsOfServiceEnabled": false, + "CustomTermsOfServiceReAcceptancePeriod": 365 + }, + "AnnouncementSettings": { + "EnableBanner": false, + "BannerText": "", + "BannerColor": "#f2a93b", + "BannerTextColor": "#333333", + "AllowBannerDismissal": true + }, + "ThemeSettings": { + "EnableThemeSelection": true, + "DefaultTheme": "default", + "AllowCustomThemes": true, + "AllowedThemes": [] + }, + "GitLabSettings": { + "Enable": true, + "Secret": "fedcba987654321fedcba987654321", + "Id": "123456789abcdef123456789abcdef", + "Scope": "", + "AuthEndpoint": "http://localhost/oauth/authorize.php", + "TokenEndpoint": "http://localhost/oauth/token.php", + "UserApiEndpoint": "http://localhost/oauth/resource.php" + }, + "GoogleSettings": { + "Enable": false, + "Secret": "", + "Id": "", + "Scope": "profile email", + "AuthEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "TokenEndpoint": "https://www.googleapis.com/oauth2/v4/token", + "UserApiEndpoint": "https://www.googleapis.com/plus/v1/people/me" + }, + "Office365Settings": { + "Enable": false, + "Secret": "", + "Id": "", + "Scope": "User.Read", + "AuthEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + "TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token", + "UserApiEndpoint": "https://graph.microsoft.com/v1.0/me" + }, + "LdapSettings": { + "Enable": false, + "EnableSync": false, + "LdapServer": "", + "LdapPort": 389, + "ConnectionSecurity": "", + "BaseDN": "", + "BindUsername": "", + "BindPassword": "", + "UserFilter": "", + "GroupFilter": "", + "GuestFilter": "", + "GroupDisplayNameAttribute": "", + "GroupIdAttribute": "", + "FirstNameAttribute": "", + "LastNameAttribute": "", + "EmailAttribute": "", + "UsernameAttribute": "", + "NicknameAttribute": "", + "IdAttribute": "", + "PositionAttribute": "", + "LoginIdAttribute": "", + "SyncIntervalMinutes": 60, + "SkipCertificateVerification": false, + "QueryTimeout": 60, + "MaxPageSize": 0, + "LoginFieldName": "", + "LoginButtonColor": "", + "LoginButtonBorderColor": "", + "LoginButtonTextColor": "", + "Trace": false + }, + "ComplianceSettings": { + "Enable": false, + "Directory": "./data/", + "EnableDaily": false + }, + "LocalizationSettings": { + "DefaultServerLocale": "en", + "DefaultClientLocale": "en", + "AvailableLocales": "" + }, + "SamlSettings": { + "Enable": false, + "EnableSyncWithLdap": false, + "EnableSyncWithLdapIncludeAuth": false, + "Verify": true, + "Encrypt": true, + "SignRequest": false, + "IdpUrl": "", + "IdpDescriptorUrl": "", + "AssertionConsumerServiceURL": "", + "SignatureAlgorithm": "RSAwithSHA1", + "CanonicalAlgorithm": "Canonical1.0", + "ScopingIDPProviderId": "", + "ScopingIDPName": "", + "IdpCertificateFile": "", + "PublicCertificateFile": "", + "PrivateKeyFile": "", + "IdAttribute": "", + "GuestAttribute": "", + "FirstNameAttribute": "", + "LastNameAttribute": "", + "EmailAttribute": "", + "UsernameAttribute": "", + "NicknameAttribute": "", + "LocaleAttribute": "", + "PositionAttribute": "", + "LoginButtonText": "With SAML", + "LoginButtonColor": "", + "LoginButtonBorderColor": "", + "LoginButtonTextColor": "" + }, + "NativeAppSettings": { + "AppDownloadLink": "https://about.mattermost.com/downloads/", + "AndroidAppDownloadLink": "https://about.mattermost.com/mattermost-android-app/", + "IosAppDownloadLink": "https://about.mattermost.com/mattermost-ios-app/" + }, + "ClusterSettings": { + "Enable": false, + "ClusterName": "", + "OverrideHostname": "", + "NetworkInterface": "", + "BindAddress": "", + "AdvertiseAddress": "", + "UseIpAddress": true, + "UseExperimentalGossip": false, + "ReadOnlyConfig": true, + "GossipPort": 8074, + "StreamingPort": 8075, + "MaxIdleConns": 100, + "MaxIdleConnsPerHost": 128, + "IdleConnTimeoutMilliseconds": 90000 + }, + "MetricsSettings": { + "Enable": false, + "BlockProfileRate": 0, + "ListenAddress": ":8067" + }, + "ExperimentalSettings": { + "ClientSideCertEnable": false, + "ClientSideCertCheck": "secondary", + "EnableClickToReply": false, + "LinkMetadataTimeoutMilliseconds": 5000, + "RestrictSystemAdmin": false + }, + "AnalyticsSettings": { + "MaxUsersForStatistics": 2500 + }, + "ElasticsearchSettings": { + "ConnectionUrl": "", + "Username": "elastic", + "Password": "changeme", + "EnableIndexing": false, + "EnableSearching": false, + "EnableAutocomplete": false, + "Sniff": true, + "PostIndexReplicas": 1, + "PostIndexShards": 1, + "ChannelIndexReplicas": 1, + "ChannelIndexShards": 1, + "UserIndexReplicas": 1, + "UserIndexShards": 1, + "AggregatePostsAfterDays": 365, + "PostsAggregatorJobStartTime": "03:00", + "IndexPrefix": "", + "LiveIndexingBatchSize": 1, + "BulkIndexingTimeWindowSeconds": 3600, + "RequestTimeoutSeconds": 30, + "SkipTLSVerification": false, + "Trace": "" + }, + "DataRetentionSettings": { + "EnableMessageDeletion": false, + "EnableFileDeletion": false, + "MessageRetentionDays": 365, + "FileRetentionDays": 365, + "DeletionJobStartTime": "02:00" + }, + "MessageExportSettings": { + "EnableExport": false, + "ExportFormat": "actiance", + "DailyRunTime": "01:00", + "ExportFromTimestamp": 0, + "BatchSize": 10000, + "GlobalRelaySettings": { + "CustomerType": "A9", + "SmtpUsername": "", + "SmtpPassword": "", + "EmailAddress": "" + } + }, + "JobSettings": { + "RunJobs": true, + "RunScheduler": true + }, + "PluginSettings": { + "Enable": true, + "EnableUploads": true, + "AllowInsecureDownloadUrl": false, + "EnableHealthCheck": true, + "Directory": "./plugins", + "ClientDirectory": "./client/plugins", + "Plugins": {}, + "PluginStates": { + "com.mattermost.nps": { + "Enable": true + } + }, + "EnableMarketplace": true, + "RequirePluginSignature": false, + "MarketplaceUrl": "https://api.integrations.mattermost.com", + "SignaturePublicKeyFiles": [] + }, + "DisplaySettings": { + "CustomUrlSchemes": [], + "ExperimentalTimezone": false + }, + "GuestAccountsSettings": { + "Enable": false, + "AllowEmailAccounts": true, + "EnforceMultifactorAuthentication": false, + "RestrictCreationToDomains": "" + }, + "ImageProxySettings": { + "Enable": false, + "ImageProxyType": "local", + "RemoteImageProxyURL": "", + "RemoteImageProxyOptions": "" + } +} diff --git a/Demo/docker-compose.yaml b/Demo/docker-compose.yaml new file mode 100644 index 0000000..ba592dd --- /dev/null +++ b/Demo/docker-compose.yaml @@ -0,0 +1,65 @@ +version: '3' +services: + nginx: + image: nginx + restart: always + ports: + - 80:80 + - 443:443 + volumes: + - ../oauth:/var/www/html/oauth + - ./nginx.conf:/etc/nginx/nginx.conf + links: + - "php:php" + + php: + build: ../Docker/php-ldap-pgsql + image: php-ldap-pgsql + volumes: + - ../oauth:/var/www/html/oauth + environment: + ldap_host: ldap://ldap.company.com:389/ + ldap_port: 389 + ldap_version: 3 + ldap_search_attribute: uid + ldap_base_dn: "ou=People,o=Company" + ldap_filter: "(objectClass=*)" + ldap_bind_dn: "" + ldap_bind_pass: "" + db_host: "127.0.0.1" + db_port: "5432" + db_type: "pgsql" + db_name: "oauth_db" + db_user: "oauth" + db_pass: "oauth_secure-pass" + + db: + image: postgres:alpine + restart: always + volumes: + - ../db_init/init_postgres.sh:/docker-entrypoint-initdb.d/init_postgres.sh + - ../db_init/config_init.sh.example:/docker-entrypoint-initdb.d/config_init.sh + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: rootroot + POSTGRES_HOST_AUTH_METHOD: trust + client_id: 123456789abcdef123456789abcdef + client_secret: fedcba987654321fedcba987654321 + redirect_uri: "http://localhost/signup/gitlab/complete" + grant_types: "authorization_code" + scope: "api" + user_id: "" + db_user: "oauth" + db_pass: "oauth_secure-pass" + db_name: "oauth_db" + db_host: "127.0.0.1" + db_port: "5432" + + mattermost: + image: mattermost/mattermost-preview + ports: + - 8065:8065 + extra_hosts: + - dockerhost:127.0.0.1 + volumes: + - ./config.json:/mm/mattermost/config/config_docker.json diff --git a/Demo/nginx.conf b/Demo/nginx.conf new file mode 100644 index 0000000..5e38a83 --- /dev/null +++ b/Demo/nginx.conf @@ -0,0 +1,136 @@ +# For more information on configuration, see: +# * Official English Documentation: http://nginx.org/en/docs/ + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/nginx/README.dynamic. +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +http { + server_names_hash_bucket_size 128; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mattermost_cache:10m max_size=3g inactive=120m use_temp_path=off; + + upstream mattermost { + server localhost:8065; + } + + server { + listen *:80; + server_name localhost; + root /var/www/html; + index index.php index.html index.htm; + + #ssl on; + #ssl_certificate ; + #ssl_certificate_key ; + + error_page 404 /404.html; + location = /40x.html { + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + } + + location ~ /api/v[0-9]+/(users/)?websocket$ { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + client_max_body_size 50M; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + proxy_buffers 256 16k; + proxy_buffer_size 16k; + proxy_read_timeout 600s; + proxy_pass http://mattermost; + } + + location /oauth/gitlab/ { + client_max_body_size 50M; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + proxy_buffers 256 16k; + proxy_buffer_size 16k; + proxy_read_timeout 600s; + proxy_cache mattermost_cache; + proxy_cache_revalidate on; + proxy_cache_min_uses 2; + proxy_cache_use_stale timeout; + proxy_cache_lock on; + proxy_pass http://mattermost; + } + + location ~ /oauth/.*\.php$ { + try_files $uri =404; + fastcgi_pass php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + location /oauth/ { + try_files $uri $uri/ =404; + } + + location / { + sub_filter 'GitLab' 'MyAuth'; + sub_filter_types *; + client_max_body_size 50M; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + proxy_buffers 256 16k; + proxy_buffer_size 16k; + proxy_read_timeout 600s; + proxy_cache mattermost_cache; + proxy_cache_revalidate on; + proxy_cache_min_uses 2; + proxy_cache_use_stale timeout; + proxy_cache_lock on; + proxy_pass http://mattermost; + } + + } + +# Uncomment following lines if you use HTTPS. These lines allows redirect to HTTPS. +# You need to change listennig port of the previous server block and to configure SSL certificates. + + #server { + # listen 80 default_server; + # server_name localhost; + # root /usr/share/nginx/html; + # index index.php index.html index.htm; + # return 301 https://$host$request_uri; + + # } +} diff --git a/Docker/README.md b/Docker/README.md index 8812ffa..b6e5bc4 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -3,26 +3,25 @@ Mattermost-LDAP - Docker module ## Summary -This repository provides necessary ressources to build the Docker image for Mattermost-LDAP module. This Docker image is usefull to try Mattermost-LDAP in a PoC, and is production ready. +This repository provides necessary ressources to build the Docker image for Mattermost-LDAP module. This Docker image is usefull to try Mattermost-LDAP in a PoC or a demo, and is production ready. ## Description -The Mattermost-LDAP module is divided into two Docker images. On the one hand, the PostgreSQL database and on the other hand, the httpd server. +The Mattermost-LDAP module can be use with Nginx or Apache/httpd. Both implementation are available in this repository. Images in this directory are used by Docker compose implementation, for more information, see section "Docker Compose setup" in the main README.md. -The PostgreSQL image installs a PostgreSQL database and then configures the Oauth user and the Oauth server database with associated tables. The Mattermost client ID, with its associated secret ID, and the Mattermost Redirect URI are added to the oauth_clients table to allow Mattermost to use the Oauth server. -The configuration of the database is done by the init.sh script whose parameters are gathered in config_init. These two files are located in the `postgres/files/` folder in this reporsitory. - -The httpd server image is based on a CentOS 7 image on which an httpd server and the necessary dependencies are installed with yum. The Oauth server is configured from the Mattermost-LDAP project available on Github. LDAP and database configuration are provided by config_db.php and config_ldap.php in the `ouath/files/` folder in this repository. +Mattermost-LDAP use a database to store tokens and users id. The Mattermost-LDAP Docker implementation is based on the official PostgreSQL server image automatically configured with the init script `init_postgres.sh` mounted in the `init.db` folder. The script is available in [init_db](../init_db/) folder. ## Architecture ![Docker Architecture of Mattermost-LDAP and interraction with Mattermost](https://github.com/Crivaledaz/Mattermost-LDAP/blob/master/Docker/mattermostldap-docker.png) -The Oauth container exposes port 80 and Postgres container port 5432. The user interacts with the Oauth server and the tokens generated by it are stored in the database. In addition, when a user logs in, his ID is stored with a unique ID. This behavior is necessary for authentication with Mattermost. The figure above illustrates interraction between Oauth server, Postgres database and Mattermost. +The Oauth container exposes port 80 and PostgreSQL container exposes port 5432. The user interacts with the Oauth server and the tokens generated by it are stored in the database. In addition, when a user logs in, his ID is stored with a unique ID. This behavior is necessary for authentication with Mattermost. The figure above illustrates interraction between Oauth server, Postgres database and Mattermost. ## Image Build -Firstly, install `docker-ce` on your host server : +You can build docker images by hand or directly use the Docker compose, which will build image automiatically. See section "Docker Compose setup" in the main README.md, for more information. To build images by hand, follow instructions below. + +First, install `docker-ce` on your host server : - For CentOS/RHEL : https://docs.docker.com/install/linux/docker-ce/centos/ - For Fedora : https://docs.docker.com/install/linux/docker-ce/fedora/ - For Debian : https://docs.docker.com/install/linux/docker-ce/debian/ @@ -31,98 +30,76 @@ Firstly, install `docker-ce` on your host server : Then, clone this repository on your host and go in `Docker` directory : ``` -git clone +git clone cd Mattermost-LDAP/Docker -``` +``` -There are two Dockerfiles, one in `postgres/` and another in `oauth/`. These Dockerfiles must be compiled to create the corresponding images. To do this, we use the docker build command as follows: +Mattermost-LDAP is available in an embedded version which uses an Apache server image (`mattermostldap`), and in a lightweight version for Nginx (`php-ldap-pgsql`) which is only an PHP image configured to enable necessary modules. + +To build mattermostldap image use the `docker build` command : ``` -docker build -t mattermostldap-postgres:latest postgres/ -docker build -t mattermostldap-oauth:latest oauth/ +docker build -t mattermostldap mattermostldap/ ``` -Once built, images are available in the Docker daemon and be used to create container with `docker run`. To view available images you can use : + +Once built, the image is available in the Docker daemon and be used to create container with `docker run`. To view available images you can use : ``` docker images list ``` +*Note* : It is recommended to use Podman on CentOS 8 and Fedora. For more information about Podman, see official documentation : https://podman.io/getting-started/ + ## Configuration Some image parameters can be changed, by specifying the desired parameters in container's environment variable, when you create a container to adapt it to your configuration. To apply custom parameters, they must be added to the container execution line with the --env (or -e) option followed by the parameter name and the desired value (-e = ). For more details, refer to the examples in the Usage section. ### LDAP -| Parameter | Description | Default value | -|-----------------------|-----------------------------------------------------------------------|--------------------------| -| ldap_host | URL or IP to connect LDAP server | ldap://ldap.company.com/ | -| ldap_port | Port used to connect LDAP server | 389 | -| ldap_version | LDAP version or protocol version used by LDAP server | 3 | -| ldap_search_attribute | Attribute used to identify a user on the LDAP | uid | -| ldap_filter | Additional filter for LDAP search | objectClass=* | -| ldap_base_dn | The base directory name of your LDAP server | ou=People,o=Company | -| ldap_bind_dn | The LDAP Directory Name of an service account to allow LDAP search | | -| ldap_bind_pass | The password associated to the service account to allow LDAP search | | +| Parameter | Description | Default value | +| --------------------- | ------------------------------------------------------------------- | -------------------------- | +| ldap_host | URL or IP to connect LDAP server | `ldap://ldap.company.com/` | +| ldap_port | Port used to connect LDAP server | `389` | +| ldap_version | LDAP version or protocol version used by LDAP server | `3` | +| ldap_search_attribute | Attribute used to identify a user on the LDAP | `uid` | +| ldap_filter | Additional filter for LDAP search | `objectClass=*` | +| ldap_base_dn | The base directory name of your LDAP server | ` ou=People,o=Company` | +| ldap_bind_dn | The LDAP Directory Name of an service account to allow LDAP search | | +| ldap_bind_pass | The password associated to the service account to allow LDAP search | | ### Database -| Parameter | Description | Default value | -|------------|----------------------------------------------------------------------|--------------------| -| db_host | Hostname or IP address of the Postgres container (database) | 127.0.0.1 | -| db_port | The port of your database to connect | 5432 | -| db_type | Database type to adapt PDO. Should be pgsql for Postgres container | pgsql | -| db_user | User who manages oauth database | oauth | -| db_pass | User's password to manage oauth database | oauth_secure-pass | -| db_name | Database name for oauth server | oauth_db | +| Parameter | Description | Default value | +| --------- | ------------------------------------------------------------------ | ------------------- | +| db_host | Hostname or IP address of the Postgres container (database) | `127.0.0.1` | +| db_port | The port of your database to connect | `5432` | +| db_type | Database type to adapt PDO. Should be pgsql for Postgres container | `pgsql` | +| db_user | User who manages oauth database | `oauth` | +| db_pass | User's password to manage oauth database | `oauth_secure-pass` | +| db_name | Database name for oauth server | `oauth_db` | ### Client -| Parameter | Description | Default value | -|-----------------|--------------------------------------------------------------------|------------------------------------------------------| -| client_id | Token client ID shared with mattermost | 123456789 | -| client_secret | Token client Secret shared with mattermost | 987654321 | -| redirect_uri | The callback address where oauth will send tokens to Mattermost | http://mattermost.company.com/signup/gitlab/complete | -| grant_types | The type of authentification use by Mattermost | authorization_code | -| scope | The scope of authentification use by Mattermost | api | -| user_id | The username of the user who create the Mattermost client in Oauth | | +| Parameter | Description | Default value | +| ------------- | ------------------------------------------------------------------ | ------------------------------------------------------ | +| client_id | Token client ID shared with mattermost | `123456789` | +| client_secret | Token client Secret shared with mattermost | `987654321` | +| redirect_uri | The callback address where oauth will send tokens to Mattermost | `http://mattermost.company.com/signup/gitlab/complete` | +| grant_types | The type of authentification use by Mattermost | `authorization_code` | +| scope | The scope of authentification use by Mattermost | `api` | +| user_id | The username of the user who create the Mattermost client in Oauth | | +*Note* : Configuration is more detailed in the main README.md. ## Usage -Both containers can be run separately, but the Mattermost-LDAP module requires both containers are working and communicating to be operational. - -### Container mattermostldap-postgres - -Once built, the mattermostldap-postgres image can be used to start a container running the postgresql database for the Mattermost-LDAP module. The image contains a default configuration specified in the configuration section. To run a container from the mattermostldap-postgres image : +The easiest way to use these images is to use the associated Docker compose file, located in the root of this repository. Alternatively, you can run only Mattermost-LDAP image, but you need an external database. +Once built, the mattermostldap image can be used to build a container running the oauth server of the Mattermost-LDAP module. The image contains a default configuration specified in the configuration section. To run a container from the mattermostldap image: ``` -docker run -d mattermostldap-postgres --name database +docker run -d mattermostldap --name oauth ``` -Image settings can be customized by passing the desired values per environment variable to the container. For example, to configure the client ID and secret ID, start the container with the following command: +To adapt the parameters of the image, you just need to specify custom parameters in environment variables of the container at its start. For example, to configure the LDAP server, use the following command: ``` -docker run -d mattermostldap-postgres --name database -e client_id=123456789 -e client_secret=987654321 -``` - -For more information about available parameters, refer to the configuration section or the [Mattermost-LDAP documentation](https://github.com/Crivaledaz/Mattermost-LDAP/blob/master/README.md). - -In addition, the mattermostldap-postgres container stores database entries in a volume outside the container to allow persistence of data beyond the life of the container. By default, Docker automatically creates a volume and stores the data in the postgresql database. However, the volume is destroyed as soon as no object references it. To overcome this problem, it is advisable to save the container data outside Docker, specifying the path of the folder that will be used for storage. To bind the container to the folder chosen, you can use this command: -``` -docker run -d mattermostldap-postgres --name database --volume /data/mattermostldap-postgres:/var/lib/postgresql/data -``` - -To delete the database container, you can use : -``` -docker rm database -``` - -## Container mattermostldap-oauth - -Once built, the mattermostldap-oauth image can be used to build a container running the oauth server of the Mattermost-LDAP module. The image contains a default configuration specified in the configuration section. To run a container from the mattermostldap-oauth image: -``` -docker run -d mattermostldap-oauth --name oauth -``` - -To adapt the parameters of the image, youjust need to specify custom parameters in environment variables of the container at its start. For example, to configure the LDAP server, use the following command: -``` -docker run -d mattermostldap-oauth --name oauth -e ldap_host="ldap.company.com" -e ldap_port=389 +docker run -d mattermostldap --name oauth -e ldap_host="ldap.company.com" -e ldap_port=389 ``` To delete the oauth container, you can use : @@ -132,6 +109,6 @@ docker rm oauth ## Improvement -In order to allow a dynamic configuration of the mattermostldap-oauth and mattermostldap-postgres images, the choice has been made to pass the parameters by environmental variables to the container. However, this method exposes all user-defined settings to all processes in the container. As a result, passwords and security tokens are exposed throughout the container and can easily be recovered by any process running in the container, including a user shell. +In order to allow a dynamic configuration of the Mattermost-LDAP module, the choice has been made to pass the parameters by environmental variables to the container. However, this method exposes all user-defined settings to all processes in the container. As a result, passwords and security tokens are exposed throughout the container and can easily be recovered by any process running in the container, including a user shell. Unfortunately, this is the simplest method to avoid defining static parameters in the image, forcing a recompilation of the image each time a value is changed. While waiting for a more secure solution, it is highly recommended to secure access to the container. diff --git a/Docker/mattermostldap/Dockerfile b/Docker/mattermostldap/Dockerfile new file mode 100644 index 0000000..ef1a8ce --- /dev/null +++ b/Docker/mattermostldap/Dockerfile @@ -0,0 +1,23 @@ +# Image mattermostldap +FROM php:apache + +RUN set -x \ + && apt-get update \ + && apt-get install -y libpq-dev libldap2-dev git\ + && rm -rf /var/lib/apt/lists/* \ + && docker-php-ext-configure pgsql --with-pgsql=/usr/local/pgsql \ + && docker-php-ext-install pdo pdo_pgsql pgsql \ + && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ \ + && docker-php-ext-install ldap + +# Get Mattermost-LDAP project +RUN git clone https://github.com/crivaledaz/Mattermost-LDAP.git /opt/Mattermost-LDAP/ + +# Install server Oauth +RUN cp -r /opt/Mattermost-LDAP/oauth/ /var/www/html/ + +# Get config file +RUN cp /var/www/html/oauth/config_db.php.example /var/www/html/oauth/config_db.php; cp /var/www/html/oauth/LDAP/config_ldap.php.example /var/www/html/oauth/LDAP/config_ldap.php + +# Open and expose port 80 for Apache server +EXPOSE 80 diff --git a/Docker/oauth/Dockerfile b/Docker/oauth/Dockerfile deleted file mode 100644 index 9ee9cda..0000000 --- a/Docker/oauth/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# mattermostldap-oauth -# Create and configure a Docker image to setup an Oauth Server (Mattermost-LDAP) -# For more information, please refer to the Mattermost-LDAP project. -# > https://github.com/crivaledaz/Mattermost-LDAP - -# Start from a CentOS 7 image -FROM centos:latest - -# Update packages and install dependencies -RUN yum update -y && yum -y install httpd php postgresql php-ldap php-pdo php-pgsql git - -# Retrieve Mattermost-LDAP from git repository -RUN git clone https://github.com/crivaledaz/Mattermost-LDAP.git Mattermost-LDAP/ - -# Change workdir -WORKDIR Mattermost-LDAP/ - -# Install server Oauth -RUN cp -r oauth/ /var/www/html/ - -# Get config files with custom parameters -ADD ./files . - -# Copy config files in Oauth server -RUN cp config_ldap.php /var/www/html/oauth/LDAP/ && cp config_db.php /var/www/html/oauth/ - -# Open and expose port 80 for Apache server -EXPOSE 80 - -# Start Apache server -CMD ["/usr/sbin/httpd", "-DFOREGROUND"] diff --git a/Docker/oauth/files/config_db.php b/Docker/oauth/files/config_db.php deleted file mode 100644 index 4da57f3..0000000 --- a/Docker/oauth/files/config_db.php +++ /dev/null @@ -1,14 +0,0 @@ - https://github.com/crivaledaz/Mattermost-LDAP - -# Start from a minimal PostgreSQL image -FROM postgres:alpine - -# Copy init script in the container -ADD ./files /docker-entrypoint-initdb.d/ - -# Prepare data for persistence -VOLUME /var/lib/postgresql/data diff --git a/LICENSE b/LICENSE index 54f21f6..7da069b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2017-2019 Denis CLAVIER +Copyright (c) 2017-2020 Denis CLAVIER Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Puppet/mattermostldap/README.md b/Puppet/mattermostldap/README.md index 3cb75cb..ad27621 100755 --- a/Puppet/mattermostldap/README.md +++ b/Puppet/mattermostldap/README.md @@ -1,6 +1,8 @@ Mattermost-LDAP Puppet Module ============================= +**WARNING** : This Puppet module is no longer supported and certainly not working with Mattermost-LDAP 2.0.0 and superior. + This is a puppet module to manage configuration and installation of Mattermost-LDAP. ## Overview @@ -18,9 +20,9 @@ The use of this puppet module substitute to the standard installation and config The Puppet Mattermost-LDAP module installs the oauth server and associated files from a release archive provided in this repository, create and configure a database for the oauth server depending on your database server (PostgreSQL or MySQL), and configures the oauth server to interact with LDAP according to settings you provide. -## Setup +## Setup ### Requirements -This module requires the following : +This module requires the following : * Puppet (3.8.7 min) * puppet/archive @@ -35,7 +37,7 @@ To know the dependencies necessary for Mattermost-LDAP (which will be installed ``` # On Puppet Client sudo yum -y --nogpgcheck install puppet -echo "server=SERVER_NAME" >> /etc/puppet/puppet.conf +echo "server=SERVER_NAME" >> /etc/puppet/puppet.conf # On Puppet Master : yum install -y --nogpgcheck puppet puppet-server @@ -54,8 +56,8 @@ puppet cert sign CLIENT_NAME ``` # On Puppet Master puppet module install puppetlabs-stdlib --version 4.17.0 -puppet module install puppet-archive --version 1.3.0 - +puppet module install puppet-archive --version 1.3.0 + ``` Your system is ready to use Puppet module Mattermost-LDAP. @@ -98,7 +100,7 @@ This will download project.tar.gz from your server, and extract the archive in / Below, there is an example of Mattermost-LDAP Puppet module using Mattermost and PostgreSQL puppet module to install and configure all running on the same server (requires puppetlabs/postgresql and liger1978/mattermost): ``` -########################---Config Mattermost---########################### +########################---Config Mattermost---########################### class { 'postgresql::server': ipv4acls => ['host all all 127.0.0.1/32 md5'], } @@ -133,8 +135,8 @@ Below, there is an example of Mattermost-LDAP Puppet module using Mattermost and }, } - ########################---Config de Oauth---########################### - + ########################---Config de Oauth---########################### + postgresql::server::db { 'oauth_db': user => 'oauth', password => postgresql_password('oauth', 'oauth_secure-pass'), @@ -166,7 +168,7 @@ Below, there is an example of Mattermost-LDAP Puppet module using Mattermost and ``` With the above code, you should be able to access the Mattermost application at http://mattermost.company.com:8065 (with your company address) and sign in with your LDAP credentials using the Gitlab button. -Please refer to ligger1978/mattermost and puppetlabs/postgresql modules in puppet forge for more information about use of these modules. +Please refer to ligger1978/mattermost and puppetlabs/postgresql modules in puppet forge for more information about use of these modules. ## Usage @@ -194,7 +196,7 @@ Your LDAP port, to connect the LDAP server. By default : 389. #### ldap_attribute (Required) The attribute used to identify user on your LDAP. Should be uid, email, cn or sAMAccountName. #### db_user (Optional) -Oauth user in the database. This user must have right on the oauth database to store oauth tokens. By default : oauth +Oauth user in the database. This user must have right on the oauth database to store oauth tokens. By default : oauth #### db_pass (Optional) Oauth user password in the database. By default, oauth_secure-pass #### db_host (Optional) @@ -204,8 +206,8 @@ The port listenning by database to connect. By default : 5432 (postgres) #### db_type (Optional) Database type to adapt script and configuration to your database server. Should be mysql or pqsql. By default : pgsql #### db_name (Optional) -Database name for oauth server. By default : oauth_db -#### client_id (Required) +Database name for oauth server. By default : oauth_db +#### client_id (Required) The application ID shared with mattermost. This ID should be a random token. You can use openssl to generate this token (openssl rand -hex 32). If the ID is not filled, database will not be initialised and client will not be created. #### client_secret (Required) The application secret shared with mattermost. This secret should be a random token. You can use openssl to generate this token (openssl rand -hex 32). If the secret is not filled, database will not be initialised and client will not be created. Secret must be different of the client ID. @@ -222,13 +224,13 @@ The date.timezone parameter for php.ini. This parameter will change the php.ini. #### ldap_bind_dn (Optional) The LDAP Directory Name of an service account to allow LDAP search. This ption is required if your LDAP is restrictive, else by default is an empty string (""). (ex : cn=mattermost_ldap,dc=Example,dc=com) #### ldap_bind_pass (Optional) -The password associated to the service account to allow LDAP search. This ption is required if your LDAP you provide an bind user, else by default is an empty string (""). +The password associated to the service account to allow LDAP search. This ption is required if your LDAP you provide an bind user, else by default is an empty string (""). ## Limitation This module has been tested on Centos 7 with PostgreSQL. -Others operating systems has not been tested yet but should work fine. +Others operating systems has not been tested yet but should work fine. MySQL has not really been tested so it is possible there is some bugs with. @@ -253,12 +255,3 @@ I wish to thank my company and my colleagues for their help and support. Also, I AllowOverride All ``` - - - - - - - - - diff --git a/README.md b/README.md index 059f82d..18e3928 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This module provides an external LDAP authentication in Mattermost for the Team ## Overview -Currently, LDAP authentication in Mattermost is not featured in the Team Edition (only in the Enterprise Edition). Thus, the only way to get LDAP authentication in Mattermost is to install Gitlab and use its Single Sign On (SSO) feature. Gitlab allows LDAP authentication and transmits user data to Mattermost. So, anyone who wishes to use LDAP with Mattermost must run Gitlab, even if he does not use it, for the SSO feature. +Currently, LDAP authentication in Mattermost is not featured in the Team Edition (only in the Enterprise Edition). Thus, the only way to get LDAP authentication in Mattermost is to install Gitlab and use its Single Sign On (SSO) feature. Gitlab allows LDAP authentication and transmits user data to Mattermost. So, anyone who wishes to use LDAP with Mattermost must run Gitlab, even if he does not use it, for the SSO feature. However, although Gitlab is a nice software, it is resources-consuming and a bit complicated to manage if you just want the SSO feature. That's the reason why, this module provides an oauth server to only reproduce the Gitlab SSO feature and allows a simple and secure LDAP authentication to Mattermost. @@ -13,15 +13,91 @@ The Mattermost-LDAP project uses the Gitlab authentication feature from Mattermo ## Module Description -This module provides an Oauth2 server designed for php, a LDAP connector for PHP and some files for automatic configuration. Once installed and configured with Mattermost, the module allows LDAP authentication by replacing Gitlab SSO. This module allows many configuration settings to try to comply with your settings and configuration. Mattermost-LDAP can be used with MySQL or PostgreSQL database on many operating systems. See Limitation section for more information. +This module provides an Oauth2 server designed for PHP, an LDAP connector for PHP and some files for automatic configuration. Once installed and configured with Mattermost, the module allows LDAP authentication by replacing Gitlab SSO. This module allows many configuration settings to try to comply with your settings and configuration. Mattermost-LDAP can be used with MySQL or PostgreSQL database on many operating systems. + +See Limitation section for more information. + +## Docker-Compose setup + +The easiest way to setup Mattermost-LDAP is using the docker-compose implementation. Two docker-compose files are available in this repository : +- `Demo/docker-compose.yaml` : To test and try Mattermost-LDAP. This docker-compose file instantiate a Mattermost Server from the official preview image provides by Mattermost and a Mattemrost-LDAP pre-configured server with a PostgreSQL database. +- `docker-compose.yaml` : For production use. This docker-compose file only setup Mattermost-LDAP with an Apache server and a PostgreSQL database. This implementation uses an embedded Oauth server, which can be configured by environment variables. -## Setup ### Requirements -This module requires the following : + +To use docker-compose implementation, you need to install Docker and Docker compose. For CentOS 8 and Fedora, it is recommended to use Podman and Podman compose instead of Docker and Docker compose. + +For more information about Docker installation, see official guide : https://docs.docker.com/engine/install/ + +For more information about Podman installation, see official documentation : https://podman.io/getting-started/installation.html + +### Preparation + +First, you need to clone (or download and extract) this repository on your server : +```bash +git clone https://github.com/Crivaledaz/Mattermost-LDAP +cd Mattermost-LDAP + +# For Demo (optional) +cd Demo +``` + +Then, before running the docker-compose file, you need to adapt LDAP and DB parameters. All parameters are gathered in the `env.example` file and they are passed to Postgres and Oauth server by environment variables. + +Copy the `env.example` file to `.env` and edit it to change with your values. For demo, parameters are directly in the `docker-compose.yaml` file, so you need to edit this file instead of `.env`. + +**Warning** : Postgres root password and database Oauth password must be changed. Client and secret tokens must be generated randomly, using `openssl rand -hex 32`. + +For more information about parameters, see beelow the configuration section of this documentation. + +Otherwise, for production, you need to create a directory to store PostgreSQL data. This directory will contain the Oauth database and allows data persistence, even if containers are stopped or restarted. By dafault, this Mattermost-LDAP implementation uses folder `data/` next to the `docker-compose.yaml` file to store data. This folder need to be created before running Docker compose : +```bash +mkdir data +``` + +For demo, you need to rename example configuration file without the example extension. +```bash +cd Mattermost-LDAP/ +cp -p oauth/config_db.php.example oauth/config_db.php +cp -p oauth/LDAP/config_ldap.php.example oauth/LDAP/config_ldap.php +``` + +To use Mattermost-LDAP with you own Mattermost server, you need to configure your Mattermost instance as described in subsection "Mattermost" in section "Configuration" + +### Usage + +Once the `.env` file have been adapted, you can run the docker-compose file with the following commands : +```bash +docker-compose build +docker-compose up -d +``` + +The build command allows Docker compose to build necessary image. Images use are available in the [Docker/](Docker) directory of this repository. The up command starts all services described in the Docker compose file. + +Once all services are started, go to Mattermost server. For the demo, Mattermost should be available on localhost : http://localhost. Click on GitLab button to login with LDAP credential on Mattermost-LDAP. Then, if you login successfully and authorize Mattermost-LDAP to transmit your data to Mattermost, you should be log on Mattermost. + +*Note* : In demo, Mattermost server is available after few seconds. + +To stop Mattermost server and Mattermost-LDAP, use the following command : +```bash +# With Docker +docker-compose down docker-compose.yaml + +# With Podman +podman-compose down docker-compose.yaml +``` + +*Note* : Docker compose setup replaces Bare-Metal setup, but configuration remains necessary. + +## Bare-Metal setup + +### Requirements + +Mattermost-LDAP requires the following : * PHP (minimum 5.3.9) * php-ldap -* php-pdo +* php-pdo * php-pgsql or php-mysql * httpd * postgresql or mariadb (mysql) @@ -37,7 +113,7 @@ Install required packages : * For Centos 7, RHEL 7 and Fedora : ```bash #For PostgreSQL -sudo yum -y --nogpgcheck install httpd php postgresql-server postgresql php-ldap php-pdo php-pgsql git +sudo yum -y --nogpgcheck install httpd php postgresql-server postgresql php-ldap php-pdo php-pgsql git #For MySQL sudo yum -y --nogpgcheck install httpd php mariadb-server mariadb php-ldap php-pdo php-mysql git @@ -46,7 +122,7 @@ sudo yum -y --nogpgcheck install httpd php mariadb-server mariadb php-ldap php-p * For Debian, ubuntu, Mint : ```bash #For PostgreSQL -sudo apt-get -y install httpd php postgresql-server postgresql php-ldap php-pdo php-pgsql git +sudo apt-get -y install httpd php postgresql-server postgresql php-ldap php-pdo php-pgsql git #For MySQL sudo apt-get -y install httpd php mariadb-server mariadb php-ldap php-pdo php-mysql git @@ -80,8 +156,9 @@ sudo systemctl enable mariadb Your system is ready to install and run Mattermost-LDAP module. -## Install -Clone (or download and extract) this repository in your `/var/www/html` (or your httpd root directory) : +### Install + +Clone (or download and extract) this repository and move `oauth` directory in `/var/www/html` (or your httpd root directory) : ```bash cd ~ git clone https://github.com/crivaledaz/Mattermost-LDAP.git @@ -89,40 +166,40 @@ cd Mattermost-LDAP cp -r oauth/ /var/www/html/ ``` -You need to create a database for the oauth server. For this purpose, you can use the script "init_postgres.sh" or "init_mysql.sh". These scripts try to configure your database automatically, by creating a new user and a new database associated for the oauth server. Scripts also create all tables necessary for the module. If script failed, please report here, and try to configure manually your database by adapting command in scripts. Before running the script you can change the default settings by editing the config_init.sh file and modifying configuration variables. For postgresql, you can copy and paste following lines : +You need to create a database for the Oauth server. For this purpose, you can use the script `init_postgres.sh` or `init_mysql.sh`, available in `db_init` directory. These scripts try to configure your database automatically, by creating a new user and a new database associated for the Oauth server. Scripts also create all tables necessary for the module. If script failed, please report here, and try to configure manually your database by adapting command in scripts. Before running the script you can change the default settings by editing the `db_init/config_init.sh` file and modifying configuration variables. For PostgreSQL, you can copy and paste following lines : ```bash -nano config_init.sh +cd db_init +vim config_init.sh ./init_postgres.sh ``` -This script will automatically create and add a new client in the oauth server, returning a client id and a client secret. You need to keep these two token to configure Mattermost. Please be sure the client secret remained secret. The redirect url in the script must comply with the hostname of your Mattermost server, else Mattermost could not get data from the Oauth server. +This script will automatically create and add a new client in the Oauth server, returning a client id and a client secret. You need to keep these two token to configure Mattermost. Please be sure the client secret remained secret. The redirect url in the script must comply with the hostname of your Mattermost server, else Mattermost could not get data from the Oauth server. ## Configuration -Configuration files are provided with examples and default values. Each config file has an ".example" extension, so you need to copy and to rename them without this extension. You can find a detailed description of each parameters available below. +Configuration files are provided with examples and default values. Each config file has an `example` extension, so you need to copy and to rename them without this extension. You can find a detailed description of each parameters available below. ### Init script parameters -| Parameter | Description | Default value | -|---------------|-----------------------------------------------------------------------|-------------------------------------------------------| -| oauth_user | Oauth user in the database. | oauth | -| oauth_pass | Oauth user password in the database. | oauth_secure-pass | -| ip | Hostname or IP address of the database. | 127.0.0.1 | -| port | The port to connect to the database. | 5432 (Postgres) | -| oauth_db_name | Database name for oauth server. | oauth_db | -| client_id | The application ID shared with mattermost. | `openssl rand -hex 32` | -| client_secret | The application secret shared with mattermost. | `openssl rand -hex 32` | -| redirect_uri | The callback address where oauth will send tokens to Mattermost. | http://mattermost.company.com/signup/gitlab/complete | -| grant_types | The type of authentification use by Mattermost. | authorization_code | -| scope | The scope of authentification use by Mattermost. | api | -| user_id | The username of the user who create the Mattermost client in Oauth. | | +| Parameter | Description | Default value | +| ------------- | ------------------------------------------------------------------- | ---------------------------------------------------- | +| db_user | Oauth user in the database. | `oauth` | +| db_pass | Oauth user password in the database. | `oauth_secure-pass` | +| db_host | Hostname or IP address of the database. | `127.0.0.1` | +| db_port | The port to connect to the database. | `5432` (Postgres) | +| db_name | Database name for Oauth server. | `oauth_db` | +| client_id | The application ID shared with mattermost. | `openssl rand -hex 32` | +| client_secret | The application secret shared with mattermost. | `openssl rand -hex 32` | +| redirect_uri | The callback address where Oauth will send tokens to Mattermost. | http://mattermost.company.com/signup/gitlab/complete | +| grant_types | The type of authentification use by Mattermost. | `authorization_code` | +| scope | The scope of authentification use by Mattermost. | `api` | +| user_id | The username of the user who create the Mattermost client in Oauth. | | -Note : The 'oauth_user' must have all privilege on the oauth database to manage oauth tokens. +*Note* : The `oauth_user` must have all privilege on the Oauth database to manage Oauth tokens. -The 'client_id' and 'client_secret' should be different and random tokens. You can use openssl to generate these tokens (`openssl rand -hex 32`). By default, these variables contain the `openssl` command, which use the openssl package. Tokens will be generated and printed at the end of the script. - -The var 'user_id' has no impact, and could be used as a commentary field. By default this field is empty. +The `client_id` and `client_secret` should be different and random tokens. You can use openssl to generate these tokens (`openssl rand -hex 32`). By default, these variables contain the `openssl` command, which use the openssl package. Tokens will be generated and printed at the end of the script. +The var `user_id` has no impact, and could be used as a commentary field. By default this field is empty. ### Mattermost @@ -134,56 +211,59 @@ Token Endpoint: http://HOSTNAME/oauth/token.php ``` Change `HOSTNAME` by hostname or ip of the server where you have installed Mattermost-LDAP module. -Since Mattermost 4.9, these fields are disabled in admin panel, so you need to edit directly the configuration file `config.json`. +Since Mattermost 4.9, these fields are disabled in admin panel, so you need to edit directly section `GitLabSettings` in the Mattermost configuration file `config.json`. ### Database credentials + Edit `oauth/config_db.php` and adapt, with your settings, to set up database in PHP. -| Parameter | Description | Default value | -|------------|----------------------------------------------------------------------|--------------------| -| db_host | Hostname or IP address of the database server | 127.0.0.1 | -| db_port | The port of your database to connect | 5432 | -| db_type | Database type to adapt PDO. Should be pgsql or mysql. | pgsql | -| db_user | User who manages oauth database | oauth | -| db_pass | User's password to manage oauth database | oauth_secure-pass | -| db_name | Database name for oauth server | oauth_db | +| Parameter | Description | Default value | +| --------- | ----------------------------------------------------- | ----------------- | +| db_host | Hostname or IP address of the database server | `127.0.0.1` | +| db_port | The port of your database to connect | `5432` | +| db_type | Database type to adapt PDO. Should be pgsql or mysql. | `pgsql` | +| db_user | User who manages Oauth database | `oauth` | +| db_pass | User's password to manage Oauth database | `oauth_secure-pass` | +| db_name | Database name for Oauth server | `oauth_db` | -If you use the init script, make sure to use the same values for database parameters : 'oauth_user' = 'db_user', 'oauth_pass' = 'db_pass', 'oauth_db_name' = 'db_name'. +If you use the init script, make sure to use the same values for database parameters. -Note : The 'db_user' must have all privilege on the oauth database to manage oauth tokens. +*Note* : The 'db_user' must have all privilege on the Oauth database to manage Oauth tokens. ### LDAP configuration -Edit `oauth/LDAP/config_ldap.php` and adapt prameters with your LDAP configuration : +Edit `oauth/LDAP/config_ldap.php` and adapt prameters with your LDAP configuration : -| Parameter | Description | Default value | -|-----------------------|-----------------------------------------------------------------------|--------------------------| -| ldap_host | URL or IP to connect LDAP server | ldap://ldap.company.com/ | -| ldap_port | Port used to connect LDAP server | 389 | -| ldap_version | LDAP version or protocol version used by LDAP server | 3 | -| ldap_search_attribute | Attribute used to identify a user on the LDAP | uid | -| ldap_filter | Additional filter for LDAP search | objectClass=* | -| ldap_base_dn | The base directory name of your LDAP server | ou=People,o=Company | -| ldap_bind_dn | The LDAP Directory Name of an service account to allow LDAP search | | -| ldap_bind_pass | The password associated to the service account to allow LDAP search | | +| Parameter | Description | Default value | +| --------------------- | ------------------------------------------------------------------- | -------------------------- | +| ldap_host | URL or IP to connect LDAP server | `ldap://ldap.company.com/` | +| ldap_port | Port used to connect LDAP server | `389` | +| ldap_version | LDAP version or protocol version used by LDAP server | `3` | +| ldap_search_attribute | Attribute used to identify a user on the LDAP | `uid` | +| ldap_filter | Additional filter for LDAP search | `(objectClass=*)` | +| ldap_base_dn | The base directory name of your LDAP server | `ou=People,o=Company` | +| ldap_bind_dn | The LDAP Directory Name of an service account to allow LDAP search | | +| ldap_bind_pass | The password associated to the service account to allow LDAP search | | For openLDAP server, the 'ldap_search_attribute' should be `uid`, and for AD server this must be `sAMAccountName`. Nevertheless, 'email' or 'cn' could be used, this depends on your LDAP configuration. Parameters 'ldap_bind_dn' and 'ldap_bind_pass' are required if your LDAP is restrictive, else put an empty string (""). -Note : 'ldap_version' avoid LDAP blind error with LDAP 3 (issue #14) +**Wraning** : Mattermost-LDAP V2 has changed 'ldap_filter' syntax. Now, the ldap filter must respect the LDAP syntax and need to be included into parenthesis. + +*Note* : 'ldap_version' avoid LDAP blind error with LDAP 3 (issue #14) To try your configuration you can use `ldap.php` available at the root of this project which use the LDAP library for PHP or you can use `ldapsearch` command in a shell. -Configure LDAP is certainly the most difficult step. - ## Usage + If you have succeeded previous step you only have to go to the login page of your Mattermost server and click on the Gitlab Button. You will be redirected to a form asking for your LDAP credentials. If your credentials are valid, you will be asked to authorize Oauth to give your information to Mattermost. After authorizing you should be redirected on Mattermost connected with your account. -Keep in mind this will create a new account on your Mattermost server with information from LDAP. The process will fail if an existing user already use your LDAP email. To bind an existing user to the LDAP authentication, sign in mattermost with this user account, go in `account settings > security > sign-in method and "switch to using Gitlab SSO"`. +Keep in mind this will create a new account on your Mattermost server with information from LDAP. The process will fail if an existing user already use your LDAP email. To bind an existing user to the LDAP authentication, sign in Mattermost with this user account, go in `account settings > security > sign-in method and "switch to using Gitlab SSO"`. ## Limitation -This module has been tested on Centos 7, Fedora and Ubuntu with PostgreSQL and Mattermost Community Edition version 4.1, 4.9, 5.0.1 and 5.10. Mattermost-LDAP is compliant with Mattermost Team Edition 4.x.x and 5.x.x. + +This module has been tested on Centos 7, Fedora and Ubuntu with PostgreSQL and Mattermost Community Edition version 4.1, 4.9, 5.0.1, 5.10, 5.15.1, 5.51.0 and 5.22.0. Mattermost-LDAP is compliant with Mattermost Team Edition 4.x.x and 5.x.x. Others operating systems has not been tested yet but should work fine. @@ -197,7 +277,7 @@ MySQL has not really been tested so it is possible there is some bugs with. ## Thanks -I wish to thank CS SI and my colleagues for their help and support. Also, I thank Brent Shaffer for his Oauth-server-php project and its documentation. +I wish to thank CS SI and my colleagues for their help and support. Also, I thank Brent Shaffer for his [Oauth-server-php](https://github.com/bshaffer/oauth2-server-php) project and its [documentation](https://bshaffer.github.io/oauth2-server-php-docs/). ## Known issues @@ -217,6 +297,3 @@ I wish to thank CS SI and my colleagues for their help and support. Also, I than AllowOverride All ``` - - - diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..eb9d5f5 --- /dev/null +++ b/changelog.md @@ -0,0 +1,23 @@ +Change Log +========== + +## Mattermost-LDAP 2.0 + +This new version of Matermost-LDAP brings many changes and some new features. + +- Update Oauth server : Integrate Oauth server from bshaffer : https://github.com/bshaffer/oauth2-server-php/releases/tag/v1.11.1 +- Compatibility with PHP 7 (issue #41) +- Add possibility to change user in the authorization page (issue #44) +- Authorization is now required only the first time (issue #45) +- New lightweight Docker image +- Docker compose implementation for easy setup +- Add a Docker-Compose demo, to allow testing Mattermost and Mattermost-LDAP in a PoC. +- Force username in lowercase (PR #40) +- Allow complex LDAP filter +- Cleaning repository. Database scripts have moved to folder `db_init` + +Breaking changes : +- LDAP filter parameter syntax has changed. Now `ldap_filter` value must use the LDAP syntax and need to be included into parenthesis. +- Some variable names have been changed, mainly parameters for database in init scripts. + +If you find a bug or a regression in this new version, let me know using issue tracker on Github : https://github.com/Crivaledaz/Mattermost-LDAP/issues diff --git a/config_init.sh.example b/config_init.sh.example deleted file mode 100755 index 5bffcea..0000000 --- a/config_init.sh.example +++ /dev/null @@ -1,16 +0,0 @@ -#####################################--CONFIGURATION FILE--######################################## - -#Client configuration -client_id=`openssl rand -hex 32` -client_secret=`openssl rand -hex 32` -redirect_uri="http://mattermost.company.com:8065/signup/gitlab/complete" -grant_types="authorization_code" -scope="api" -user_id="" - -#Database configuration -oauth_user="oauth" -oauth_db_name="oauth_db" -oauth_pass="oauth_secure-pass" -ip="127.0.0.1" -port="5432" \ No newline at end of file diff --git a/Docker/postgres/files/config_init.sh b/db_init/config_init.sh.example old mode 100644 new mode 100755 similarity index 63% rename from Docker/postgres/files/config_init.sh rename to db_init/config_init.sh.example index c1debcb..ab92836 --- a/Docker/postgres/files/config_init.sh +++ b/db_init/config_init.sh.example @@ -9,8 +9,8 @@ scope=$(if [ -z $scope ]; then echo "api"; else echo $client_id; fi) user_id=$(if [ -z $user_id ]; then echo ""; else echo $user_id; fi) #Database configuration -oauth_user=$(if [ -z $oauth_user ]; then echo "oauth"; else echo $oauth_user; fi) -oauth_db_name=$(if [ -z $oauth_db_name ]; then echo "oauth_db"; else echo $oauth_db_name; fi) -oauth_pass=$(if [ -z $oauth_pass ]; then echo "oauth_secure-pass"; else echo $oauth_pass; fi) -ip=$(if [ -z $db_host ]; then echo "localhost"; else echo $ip; fi) -port=$(if [ -z $db_port ]; then echo "5432"; else echo $port; fi) +db_user=$(if [ -z $db_user ]; then echo "oauth"; else echo $db_user; fi) +db_name=$(if [ -z $db_name ]; then echo "oauth_db"; else echo $db_name; fi) +db_pass=$(if [ -z $db_pass ]; then echo "oauth_secure-pass"; else echo $db_pass; fi) +db_host=$(if [ -z $db_host ]; then echo "localhost"; else echo $db_host; fi) +db_port=$(if [ -z $db_port ]; then echo "5432"; else echo $db_port; fi) diff --git a/init_mysql.sh b/db_init/init_mysql.sh similarity index 74% rename from init_mysql.sh rename to db_init/init_mysql.sh index cf0f36a..da29c92 100755 --- a/init_mysql.sh +++ b/db_init/init_mysql.sh @@ -3,7 +3,7 @@ source config_init.sh -#If script does not work, fill the following variable with the mysql account password +#If script does not work, fill the following variable with the mysql account password mysql_pass="" #######################################--Fonctions--############################################### @@ -35,26 +35,26 @@ sleep 5 #Creating Oauth role and associated database (need admin account on mysql) -info "Creation of role $oauth_user and database $oauth_db ... (need to be root)" -sudo mysql -u root --password=$mysql_pass --execute "CREATE DATABASE $oauth_db_name;" -sudo mysql -u root --password=$mysql_pass --execute "CREATE USER $oauth_user@'%' IDENTIFIED BY '$oauth_pass';" -sudo mysql -u root --password=$mysql_pass --execute "GRANT ALL PRIVILEGES ON $oauth_db_name.* TO $oauth_user@'%';" +info "Creation of role $db_user and database $db_name ... (need to be root)" +sudo mysql -u root --password=$mysql_pass --execute "CREATE DATABASE $db_name_name;" +sudo mysql -u root --password=$mysql_pass --execute "CREATE USER $db_user@'%' IDENTIFIED BY '$db_pass';" +sudo mysql -u root --password=$mysql_pass --execute "GRANT ALL PRIVILEGES ON $db_name_name.* TO $db_user@'%';" #Creating tables for ouath database (use oauth role) -info "Creation of tables for database $oauth_db (using $oauth_user)" -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "$create_table_oauth_client" -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "$create_table_oauth_access_tokens" -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "$create_table_oauth_authorization_codes" -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "$create_table_oauth_refresh_tokens" -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "$create_table_users" -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "$create_table_oauth_scopes" +info "Creation of tables for database $db_name (using $db_user)" +mysql -u $db_user --password=$db_pass $db_name_name --execute "$create_table_oauth_client" +mysql -u $db_user --password=$db_pass $db_name_name --execute "$create_table_oauth_access_tokens" +mysql -u $db_user --password=$db_pass $db_name_name --execute "$create_table_oauth_authorization_codes" +mysql -u $db_user --password=$db_pass $db_name_name --execute "$create_table_oauth_refresh_tokens" +mysql -u $db_user --password=$db_pass $db_name_name --execute "$create_table_users" +mysql -u $db_user --password=$db_pass $db_name_name --execute "$create_table_oauth_scopes" #Insert new client in the database info "Insert new client in the database" -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "$create_client" +mysql -u $db_user --password=$db_pass $db_name_name --execute "$create_client" #Verification -mysql -u $oauth_user --password=$oauth_pass $oauth_db_name --execute "SELECT * from oauth_clients WHERE client_id='$client_id';" | grep '(1' +mysql -u $db_user --password=$db_pass $db_name_name --execute "SELECT * from oauth_clients WHERE client_id='$client_id';" | grep '(1' if [ $? ] then ok "Client has been created ! Oauth Database is configured.\n" @@ -63,4 +63,4 @@ warn "Client Secret : $client_secret\n" info "Keep id and secret, you will need them to configure Mattermost" warn "Beware Client Secret IS PRIVATE and MUST BE KEPT SECRET" else error "Client has not been created ! Check log below" -fi \ No newline at end of file +fi diff --git a/Docker/postgres/files/init.sh b/db_init/init_postgres.sh old mode 100644 new mode 100755 similarity index 75% rename from Docker/postgres/files/init.sh rename to db_init/init_postgres.sh index 962b876..44c6c85 --- a/Docker/postgres/files/init.sh +++ b/db_init/init_postgres.sh @@ -1,6 +1,8 @@ #!/bin/bash #This script need right to become postgres user (so root) and to read/write in httpd directory +source config_init.sh + #######################################--Fonctions--############################################### ok() { echo -e '\e[32m'$1'\e[m'; } @@ -31,26 +33,26 @@ info "Press ctrl+c to stop the script" sleep 5 #Creating Oauth role and associated database (need admin account on postgres) -info "Creation of role $oauth_user and database $oauth_db ... (need to be root)" -psql -U postgres -c "CREATE DATABASE $oauth_db_name;" -psql -U postgres -c "CREATE USER $oauth_user WITH ENCRYPTED PASSWORD '$oauth_pass';" -psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE $oauth_db_name TO $oauth_user;" +info "Creation of role $db_user and database $db_name ..." +psql -U postgres -c "CREATE DATABASE $db_name;" +psql -U postgres -c "CREATE USER $db_user WITH ENCRYPTED PASSWORD '$db_pass';" +psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE $db_name TO $db_user;" #Creating tables for ouath database (use oauth role) -info "Creation of tables for database $oauth_db (using $oauth_user)" -psql -U $oauth_user -d $oauth_db_name -c "$create_table_oauth_client" -psql -U $oauth_user -d $oauth_db_name -c "$create_table_oauth_access_tokens" -psql -U $oauth_user -d $oauth_db_name -c "$create_table_oauth_authorization_codes" -psql -U $oauth_user -d $oauth_db_name -c "$create_table_oauth_refresh_tokens" -psql -U $oauth_user -d $oauth_db_name -c "$create_table_users" -psql -U $oauth_user -d $oauth_db_name -c "$create_table_oauth_scopes" +info "Creation of tables for database $db_name (using $db_user)" +psql -U $db_user -d $db_name -c "$create_table_oauth_client" +psql -U $db_user -d $db_name -c "$create_table_oauth_access_tokens" +psql -U $db_user -d $db_name -c "$create_table_oauth_authorization_codes" +psql -U $db_user -d $db_name -c "$create_table_oauth_refresh_tokens" +psql -U $db_user -d $db_name -c "$create_table_users" +psql -U $db_user -d $db_name -c "$create_table_oauth_scopes" #Insert new client in the database info "Insert new client in the database" -psql -U $oauth_user -d $oauth_db_name -c "$create_client" +psql -U $db_user -d $db_name -c "$create_client" #Verification -psql -U $oauth_user -d $oauth_db_name -c "SELECT * from oauth_clients WHERE client_id='$client_id';" | grep '(1' +psql -U $db_user -d $db_name -c "SELECT * from oauth_clients WHERE client_id='$client_id';" | grep '(1' if [ $? ] then ok "Client has been created ! Oauth Database is configured.\n" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..4033a8b --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,47 @@ +version: '3' +services: + mattermost-ldap: + build: Docker/mattermostldap + image: mattermostldap + restart: always + ports: + - 80:80 + - 443:443 + environment: + - ldap_host + - ldap_port + - ldap_version + - ldap_search_attribute + - ldap_base_dn + - ldap_filter + - ldap_bind_dn + - ldap_bind_pass + - db_host + - db_port + - db_type + - db_name + - db_user + - db_pass + + db: + image: postgres:alpine + restart: always + volumes: + - ./db_init/init_postgres.sh:/docker-entrypoint-initdb.d/init_postgres.sh + - ./db_init/config_init.sh.example:/docker-entrypoint-initdb.d/config_init.sh + - ./data/:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER + - POSTGRES_PASSWORD + - POSTGRES_HOST_AUTH_METHOD + - client_id + - client_secret + - redirect_uri + - grant_types + - scope + - user_id + - db_user + - db_pass + - db_name + - db_host + - db_port diff --git a/env.example b/env.example new file mode 100644 index 0000000..1f10418 --- /dev/null +++ b/env.example @@ -0,0 +1,85 @@ +# Docker compose parameters for Mattermost-LDAP +# +# Adapt these parameters to match with your configuration. +# More information available in section "Configuration" in README.md + +# +# Oauth client configuration +# + +# Client ID token. Must be a random hex value. Use `openssl rand -hex 32` to generate a token. +client_id = 123456789abcdef123456789abcdef + +# Client Secret token. Must be a random hex value. Use `openssl rand -hex 32` to generate a token. +client_secret = fedcba987654321fedcba987654321 + +# Redirect URI use by Oauth server to redirect user after authentifictaion process. Must be the same than as Mattermost give to Oauth server. +redirect_uri = "http://localhost/signup/gitlab/complete" + +# Grant types method uses by Oauth server +grant_types = "authorization_code" + +# Scope of the client in the Oauth server +scope = "api" + +# Non important parameter. Could be used as a commentary field +user_id = "" + +# +# Database configuration +# + +# Username for the PostgreSQL administrator account +POSTGRES_USER = postgres + +# Password for PostgreSQL administrator account +POSTGRES_PASSWORD = rootroot + +# Method to use for connection to database +POSTGRES_HOST_AUTH_METHOD = trust + +# Oauth user to connect the database +db_user = "oauth" + +# Oauth password to connect the database +db_pass = "oauth_secure-pass" + +# Oauth database name +db_name = "oauth_db" + +# PostgreSQL database host +db_host = "127.0.0.1" + +# PostgreSQL database port +db_port = "5432" + +# Database type. Docker compose implementation for Mattermost-LDAP uses PostgreSQL. +db_type = "pgsql" + +# +# LDAP configuration +# + +# LDAP host or IP +ldap_host = ldap://ldap.company.com:389/ + +# LDAP port +ldap_port = 389 + +# LDAP protocol version +ldap_version = 3 + +# Unique identifier for entry in LDAP +ldap_search_attribute = uid + +# Base DN to search from in LDAP +ldap_base_dn = "ou=People,o=Company" + +# Additional filter for LDAP search +ldap_filter = "(objectClass=*)" + +# Service account to bind LDAP server +ldap_bind_dn = "" + +# Password for service account to bind LDAP server +ldap_bind_pass = "" diff --git a/init_postgres.sh b/init_postgres.sh deleted file mode 100755 index 81e7958..0000000 --- a/init_postgres.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash -#This script need right to become postgres user (so root) and to read/write in httpd directory - -source config_init.sh - -#######################################--Fonctions--############################################### - -ok() { echo -e '\e[32m'$1'\e[m'; } -error() { echo -e '\e[31m'$1'\e[m'; } -info() { echo -e '\e[34m'$1'\e[m'; } -warn() { echo -e '\e[33m'$1'\e[m'; } - -#######################################--SQL STATEMENT--########################################### - -#Tables creation -create_table_oauth_client="CREATE TABLE oauth_clients (client_id VARCHAR(80) NOT NULL, client_secret VARCHAR(80), redirect_uri VARCHAR(2000) NOT NULL, grant_types VARCHAR(80), scope VARCHAR(100), user_id VARCHAR(80), CONSTRAINT clients_client_id_pk PRIMARY KEY (client_id));" -create_table_oauth_access_tokens="CREATE TABLE oauth_access_tokens (access_token VARCHAR(40) NOT NULL, client_id VARCHAR(80) NOT NULL, user_id VARCHAR(255), expires TIMESTAMP NOT NULL, scope VARCHAR(2000), CONSTRAINT access_token_pk PRIMARY KEY (access_token));" -create_table_oauth_authorization_codes="CREATE TABLE oauth_authorization_codes (authorization_code VARCHAR(40) NOT NULL, client_id VARCHAR(80) NOT NULL, user_id VARCHAR(255), redirect_uri VARCHAR(2000), expires TIMESTAMP NOT NULL, scope VARCHAR(2000), CONSTRAINT auth_code_pk PRIMARY KEY (authorization_code));" -create_table_oauth_refresh_tokens="CREATE TABLE oauth_refresh_tokens (refresh_token VARCHAR(40) NOT NULL, client_id VARCHAR(80) NOT NULL, user_id VARCHAR(255), expires TIMESTAMP NOT NULL, scope VARCHAR(2000), CONSTRAINT refresh_token_pk PRIMARY KEY (refresh_token));" -create_table_users="CREATE TABLE users (id SERIAL NOT NULL, username VARCHAR(255) NOT NULL, CONSTRAINT id_pk PRIMARY KEY (id));" -create_table_oauth_scopes="CREATE TABLE oauth_scopes (scope TEXT, is_default BOOLEAN);" - -#Client creation -create_client="INSERT INTO oauth_clients (client_id,client_secret,redirect_uri,grant_types,scope,user_id) VALUES ('$client_id','$client_secret','$redirect_uri','$grant_types','$scope','$user_id');" - -################################################################################################### - -#Welcome Message -info "This script will create a new Oauth role and an associated database for Mattermost-LDAP\nTo edit configuration please edit this script before running !\n" -warn "SuperUser right must be ask to create the new role and database in postgres\n" -info "Press ctrl+c to stop the script" -sleep 5 - - -#Creating Oauth role and associated database (need admin account on postgres) -info "Creation of role $oauth_user and database $oauth_db ... (need to be root)" -sudo -S -u postgres psql -c "CREATE DATABASE $oauth_db_name;" -sudo -S -u postgres psql -c "CREATE USER $oauth_user WITH ENCRYPTED PASSWORD '$oauth_pass';" -sudo -S -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $oauth_db_name TO $oauth_user;" - -#Creating tables for ouath database (use oauth role) -info "Creation of tables for database $oauth_db (using $oauth_user)" -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "$create_table_oauth_client" -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "$create_table_oauth_access_tokens" -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "$create_table_oauth_authorization_codes" -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "$create_table_oauth_refresh_tokens" -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "$create_table_users" -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "$create_table_oauth_scopes" - -#Insert new client in the database -info "Insert new client in the database" -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "$create_client" - -#Verification -psql postgres://$oauth_user:$oauth_pass@$ip:$port/$oauth_db_name -c "SELECT * from oauth_clients WHERE client_id='$client_id';" | grep '(1' - -if [ $? ] -then ok "Client has been created ! Oauth Database is configured.\n" -info "Client ID : $client_id" -warn "Client Secret : $client_secret\n" -info "Keep id and secret, you will need them to configure Mattermost" -warn "Beware Client Secret IS PRIVATE and MUST BE KEPT SECRET" -else error "Client has not been created ! Check log below" -fi diff --git a/oauth/.htaccess b/oauth/.htaccess index 59b6f49..3d8e7d7 100644 --- a/oauth/.htaccess +++ b/oauth/.htaccess @@ -5,3 +5,13 @@ deny from all allow from all + +# Only allow access to CSS files + + allow from all + + +# Only allow access to image + + allow from all + diff --git a/oauth/LDAP/LDAP.php b/oauth/LDAP/LDAP.php index 30ecca8..d2c5e49 100755 --- a/oauth/LDAP/LDAP.php +++ b/oauth/LDAP/LDAP.php @@ -1,7 +1,7 @@ */ @@ -13,239 +13,204 @@ class LDAP implements LDAPInterface { protected $ldap_server; - /** - * LDAP Resource - * - * @param string @ldap_host - * Either a hostname or, with OpenLDAP 2.x.x and later, a full LDAP URI - * @param int @ldap_port - * An optional int to specify ldap server port, by default : 389 - * @param int @ldap_version - * An optional int to specify ldap version, by default LDAP V3 protocol is used - * - * Initiate LDAP connection by creating an associated resource - */ + /** + * LDAP Resource + * + * @param string @ldap_host + * Either a hostname or, with OpenLDAP 2.x.x and later, a full LDAP URI + * @param int @ldap_port + * An optional int to specify ldap server port, by default : 389 + * @param int @ldap_version + * An optional int to specify ldap version, by default LDAP V3 protocol is used + * + * Initiate LDAP connection by creating an associated resource + */ public function __construct($ldap_host, $ldap_port = 389, $ldap_version = 3) { - if (!is_string($ldap_host)) - { + if (!is_string($ldap_host)) { throw new InvalidArgumentException('First argument to LDAP must be the hostname of a ldap server (string). Ex: ldap//example.com/ '); } - - if (!is_int($ldap_port)) - { + + if (!is_int($ldap_port)) { throw new InvalidArgumentException('Second argument to LDAP must be the ldap server port (int). Ex : 389'); } - $ldap = ldap_connect($ldap_host, $ldap_port) - or die("Unable to connect to the ldap server : $ldaphost ! Please check your configuration."); + $ldap = ldap_connect($ldap_host, $ldap_port) + or die("Unable to connect to the ldap server : $ldaphost ! Please check your configuration."); // Support LDAP V3 since many users have encountered difficulties with LDAP V3. - if (is_int($ldap_version) && $ldap_version <= 3 && $ldap_version > 0) - { + if (is_int($ldap_version) && $ldap_version <= 3 && $ldap_version > 0) { ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, $ldap_version); - } - else - { + } else { throw new InvalidArgumentException('Third argument to LDAP must be the ldap version (int). Ex : 3'); } $this->ldap_server = $ldap; } - /** - * @param string @user - * A ldap username or email or sAMAccountName - * @param string @password - * An optional password linked to the user, if not provided an anonymous bind is attempted - * @param string @ldap_search_attribute - * The attribute used on your LDAP to identify user (uid, email, cn, sAMAccountName) - * @param string @ldap_filter - * An optional filter to search in LDAP (ex : objectClass = person). - * @param string @ldap_base_dn - * The LDAP base DN. - * @param string @ldap_bind_dn - * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. - * @param string @ldap_bind_pass - * The password associated to the service user to bind before search. - * - * @return - * TRUE if the user is identified and can access to the LDAP server - * and FALSE if it isn't - */ - public function checkLogin($user, $password = null, $ldap_search_attribute, $ldap_filter = null, $ldap_base_dn,$ldap_bind_dn, $ldap_bind_pass) { - if (!is_string($user)) - { + /** + * @param string @user + * A ldap username or email or sAMAccountName + * @param string @password + * An optional password linked to the user, if not provided an anonymous bind is attempted + * @param string @ldap_search_attribute + * The attribute used on your LDAP to identify user (uid, email, cn, sAMAccountName) + * @param string @ldap_filter + * An optional filter to search in LDAP (ex : objectClass = person). + * @param string @ldap_base_dn + * The LDAP base DN. + * @param string @ldap_bind_dn + * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. + * @param string @ldap_bind_pass + * The password associated to the service user to bind before search. + * + * @return + * TRUE if the user is identified and can access to the LDAP server + * and FALSE if it isn't + */ + public function checkLogin($user, $password = null, $ldap_search_attribute, $ldap_filter = null, $ldap_base_dn, $ldap_bind_dn, $ldap_bind_pass) + { + if (!is_string($user)) { throw new InvalidArgumentException('First argument to LDAP/checkLogin must be the username or email of a ldap user (string). Ex: jdupont or jdupont@company.com'); } - if (!is_string($password) && $password != null) - { + if (!is_string($password) && $password != null) { throw new InvalidArgumentException('Second argument to LDAP/checkLogin must be the password associated to the relative directory name (string).'); - } - if (!is_string($ldap_search_attribute)) - { + } + if (!is_string($ldap_search_attribute)) { throw new InvalidArgumentException('Third argument to LDAP/checkLogin must be the attribute to identify users (ex : uid, email, sAMAccountName) (string).'); } - if (!is_string($ldap_filter) && $ldap_filter != null) - { + if (!is_string($ldap_filter) && $ldap_filter != null) { throw new InvalidArgumentException('Fourth argument to LDAP/checkLogin must be an optional filter to search in LDAP (string).'); } - if (!is_string($ldap_base_dn)) - { + if (!is_string($ldap_base_dn)) { throw new InvalidArgumentException('Fifth argument to LDAP/checkLogin must be the ldap base directory name (string). Ex: o=Company'); } - if (!is_string($ldap_bind_dn) && $ldap_bind_dn != null) - { + if (!is_string($ldap_bind_dn) && $ldap_bind_dn != null) { throw new InvalidArgumentException('Sixth argument to LDAP/checkLogin must be an optional service account on restrictive LDAP (string).'); } - if (!is_string($ldap_bind_pass) && $ldap_bind_pass != null) - { + if (!is_string($ldap_bind_pass) && $ldap_bind_pass != null) { throw new InvalidArgumentException('Seventh argument to LDAP/checkLogin must be an optional password for the service account on restrictive LDAP (string).'); } - - // If LDAP service account for search is specified, do an ldap_bind with this account - if ($ldap_bind_dn != '' && $ldap_bind_dn != null) - { - $bind_result=ldap_bind($this->ldap_server,$ldap_bind_dn,$ldap_bind_pass); - // If authentification failed, throw an exception - if (!$bind_result) - { + // If LDAP service account for search is specified, do an ldap_bind with this account + if ($ldap_bind_dn != '' && $ldap_bind_dn != null) { + $bind_result=ldap_bind($this->ldap_server, $ldap_bind_dn, $ldap_bind_pass); + + // If authentification failed, throw an exception + if (!$bind_result) { throw new Exception('An error has occured during ldap_bind execution. Please check parameter of LDAP/checkLogin, and make sure that user provided have read permission on LDAP.'); } } - if ($ldap_filter!="" && $ldap_filter != null) - { - $search_filter = '(&(' . $ldap_search_attribute . '=' . $user . ')(' . $ldap_filter .'))'; - } - else - { - $search_filter = $ldap_search_attribute . '=' . $user; + if ($ldap_filter!="" && $ldap_filter != null) { + $search_filter = '(&(' . $ldap_search_attribute . '=' . $user . ')' . $ldap_filter .')'; + } else { + $search_filter = '(' . $ldap_search_attribute . '=' . $user . ')'; } - + $result = ldap_search($this->ldap_server, $ldap_base_dn, $search_filter, array(), 0, 1, 500); - if (!$result) - { + if (!$result) { throw new Exception('An error has occured during ldap_search execution. Please check parameter of LDAP/checkLogin.'); } $data = ldap_first_entry($this->ldap_server, $result); - if (!$data) - { + if (!$data) { throw new Exception('An error has occured during ldap_first_entry execution. Please check parameter of LDAP/checkLogin.'); } $dn = ldap_get_dn($this->ldap_server, $data); - if (!$dn) - { + if (!$dn) { throw new Exception('An error has occured during ldap_get_values execution (dn). Please check parameter of LDAP/checkLogin.'); } - return ldap_bind($this->ldap_server,$dn,$password); + return ldap_bind($this->ldap_server, $dn, $password); } - /** - * @param string @ldap_base_dn - * The LDAP base DN. - * @param string @ldap_filter - * A filter to get relevant data. Often the user id in ldap (uid or sAMAccountName). - * @param string @ldap_bind_dn - * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. - * @param string @ldap_bind_pass - * The password associated to the service user to bind before search. - * @param string @ldap_search_attribute - * The attribute used on your LDAP to identify user (uid, email, cn, sAMAccountName) - * @param string @user - * A ldap username or email or sAMAccountName - * - * @return - * An array with the user's mail, complete name and directory name. - */ - public function getDataForMattermost($ldap_base_dn, $ldap_filter, $ldap_bind_dn, $ldap_bind_pass, $ldap_search_attribute, $user) { + /** + * @param string @ldap_base_dn + * The LDAP base DN. + * @param string @ldap_filter + * A filter to get relevant data. Often the user id in ldap (uid or sAMAccountName). + * @param string @ldap_bind_dn + * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. + * @param string @ldap_bind_pass + * The password associated to the service user to bind before search. + * @param string @ldap_search_attribute + * The attribute used on your LDAP to identify user (uid, email, cn, sAMAccountName) + * @param string @user + * A ldap username or email or sAMAccountName + * + * @return + * An array with the user's mail, complete name and directory name. + */ + public function getDataForMattermost($ldap_base_dn, $ldap_filter, $ldap_bind_dn, $ldap_bind_pass, $ldap_search_attribute, $user) + { + $attribute=array("cn","mail"); - $attribute=array("cn","mail"); - - if (!is_string($ldap_base_dn)) - { + if (!is_string($ldap_base_dn)) { throw new InvalidArgumentException('First argument to LDAP/getData must be the ldap base directory name (string). Ex: o=Company'); } - if (!is_string($ldap_filter)) - { + if (!is_string($ldap_filter)) { throw new InvalidArgumentException('Second argument to LDAP/getData must be a filter to get relevant data. Often is the user id in ldap (string). Ex : uid=jdupont'); } - if (!is_string($ldap_bind_dn) && $ldap_bind_dn != null) - { + if (!is_string($ldap_bind_dn) && $ldap_bind_dn != null) { throw new InvalidArgumentException('Third argument to LDAP/getData must be an optional service account on restrictive LDAP (string).'); } - if (!is_string($ldap_bind_pass) && $ldap_bind_pass != null) - { + if (!is_string($ldap_bind_pass) && $ldap_bind_pass != null) { throw new InvalidArgumentException('Fourth argument to LDAP/getData must be an optional password for the service account on restrictive LDAP (string).'); } - if (!is_string($ldap_search_attribute)) - { + if (!is_string($ldap_search_attribute)) { throw new InvalidArgumentException('Fifth argument to LDAP/getData must be the attribute to identify users (ex : uid, email, sAMAccountName) (string).'); } - if (!is_string($user)) - { + if (!is_string($user)) { throw new InvalidArgumentException('Sixth argument to LDAP/getData must be the username or email of a ldap user (string). Ex: jdupont or jdupont@company.com'); } // If LDAP service account for search is specified, do an ldap_bind with this account - if ($ldap_bind_dn != '' && $ldap_bind_dn != null) - { - $bind_result=ldap_bind($this->ldap_server,$ldap_bind_dn,$ldap_bind_pass); + if ($ldap_bind_dn != '' && $ldap_bind_dn != null) { + $bind_result=ldap_bind($this->ldap_server, $ldap_bind_dn, $ldap_bind_pass); - // If authentification failed, throw an exception - if (!$bind_result) - { + // If authentification failed, throw an exception + if (!$bind_result) { throw new Exception('An error has occured during ldap_bind execution. Please check parameter of LDAP/getData, and make sure that user provided have read permission on LDAP.'); } } - if ($ldap_filter!="" && $ldap_filter != null) - { - $search_filter = '(&(' . $ldap_search_attribute . '=' . $user . ')(' . $ldap_filter .'))'; + if ($ldap_filter!="" && $ldap_filter != null) { + $search_filter = '(&(' . $ldap_search_attribute . '=' . $user . ')' . $ldap_filter .')'; + } else { + $search_filter = '(' . $ldap_search_attribute . '=' . $user . ')'; } - else - { - $search_filter = $ldap_search_attribute . '=' . $user; - } - + $result = ldap_search($this->ldap_server, $ldap_base_dn, $search_filter, array(), 0, 1, 500); - if (!$result) - { - throw new Exception('An error has occured during ldap_search execution. Please check parameter of LDAP/getData.'); + if (!$result) { + throw new Exception('An error has occured during ldap_search execution. Please check parameter of LDAP/getData.'); } $data = ldap_first_entry($this->ldap_server, $result); - if (!$data) - { - throw new Exception('An error has occured during ldap_first_entry execution. Please check parameter of LDAP/getData.'); + if (!$data) { + throw new Exception('An error has occured during ldap_first_entry execution. Please check parameter of LDAP/getData.'); } $mail = ldap_get_values($this->ldap_server, $data, "mail"); - if (!$mail) - { - throw new Exception('An error has occured during ldap_get_values execution (mail). Please check parameter of LDAP/getData.'); + if (!$mail) { + throw new Exception('An error has occured during ldap_get_values execution (mail). Please check parameter of LDAP/getData.'); } $cn = ldap_get_values($this->ldap_server, $data, "cn"); - if (!$cn) - { - throw new Exception('An error has occured during ldap_get_values execution (complete name). Please check parameter of LDAP/getData.'); + if (!$cn) { + throw new Exception('An error has occured during ldap_get_values execution (complete name). Please check parameter of LDAP/getData.'); } return array("mail" => $mail[0], "cn" => $cn[0]); } /* - * Destructor to close the LDAP connection + * Destructor to close the LDAP connection */ - public function __destruct() + public function __destruct() { - ldap_close($this->ldap_server); + ldap_close($this->ldap_server); } - } diff --git a/oauth/LDAP/LDAPInterface.php b/oauth/LDAP/LDAPInterface.php index 09e4f25..012d836 100755 --- a/oauth/LDAP/LDAPInterface.php +++ b/oauth/LDAP/LDAPInterface.php @@ -3,47 +3,47 @@ /** * Class to interact with LDAP * - * @author Denis CLAVIER + * @author Denis CLAVIER */ interface LDAPInterface { - /** - * @param string @user - * A ldap username or email or sAMAccountName - * @param string @password - * An optional password linked to the user, if not provided an anonymous bind is attempted - * @param string @ldap_search_attribute - * The attribute used on your LDAP to identify user (uid, email, cn, sAMAccountName) - * @param string @ldap_filter - * An optional filter to search in LDAP (ex : objectClass = person). - * @param string @ldap_base_dn - * The LDAP base DN. - * @param string @ldap_bind_dn - * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. - * @param string @ldap_bind_pass - * The password associated to the service user to bind before search. - * - * @return - * TRUE if the user is identified and can access to the LDAP server - * and FALSE if it isn't - */ - public function checkLogin($user,$password = null,$ldap_search_attribute,$ldap_filter = null,$ldap_base_dn,$ldap_bind_dn,$ldap_bind_pass); + /** + * @param string @user + * A ldap username or email or sAMAccountName + * @param string @password + * An optional password linked to the user, if not provided an anonymous bind is attempted + * @param string @ldap_search_attribute + * The attribute used on your LDAP to identify user (uid, email, cn, sAMAccountName) + * @param string @ldap_filter + * An optional filter to search in LDAP (ex : objectClass = person). + * @param string @ldap_base_dn + * The LDAP base DN. + * @param string @ldap_bind_dn + * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. + * @param string @ldap_bind_pass + * The password associated to the service user to bind before search. + * + * @return + * TRUE if the user is identified and can access to the LDAP server + * and FALSE if it isn't + */ + public function checkLogin($user, $password = null, $ldap_search_attribute, $ldap_filter = null, $ldap_base_dn, $ldap_bind_dn, $ldap_bind_pass); /** * @param string @ldap_base_dn - * The LDAP base DN. + * The LDAP base DN. * @param string @ldap_filter * A filter to get relevant data. Often the user id in ldap (uid or sAMAccountName). * @param string @ldap_bind_dn - * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. + * The directory name of a service user to bind before search. Must be a user with read permission on LDAP. * @param string @ldap_bind_pass * The password associated to the service user to bind before search. * @param string @ldap_search_attribute * The attribute used on your LDAP to identify user (uid, email, cn, sAMAccountName) * @param string @user - * A ldap username or email or sAMAccountName - * - * @return + * A ldap username or email or sAMAccountName + * + * @return * An array with the user's mail, complete name and directory name. */ public function getDataForMattermost($ldap_base_dn, $ldap_filter, $ldap_bind_dn, $ldap_bind_pass, $ldap_search_attribute, $user); diff --git a/oauth/LDAP/config_ldap.php.example b/oauth/LDAP/config_ldap.php.example index cc35997..a570166 100755 --- a/oauth/LDAP/config_ldap.php.example +++ b/oauth/LDAP/config_ldap.php.example @@ -1,16 +1,16 @@ deny from all - \ No newline at end of file + diff --git a/oauth/OAuth2/Autoloader.php b/oauth/OAuth2/Autoloader.php index ecfb6ba..4ec08cb 100644 --- a/oauth/OAuth2/Autoloader.php +++ b/oauth/OAuth2/Autoloader.php @@ -10,8 +10,14 @@ namespace OAuth2; */ class Autoloader { + /** + * @var string + */ private $dir; + /** + * @param string $dir + */ public function __construct($dir = null) { if (is_null($dir)) { @@ -19,6 +25,7 @@ class Autoloader } $this->dir = $dir; } + /** * Registers OAuth2\Autoloader as an SPL autoloader. */ @@ -31,9 +38,8 @@ class Autoloader /** * Handles autoloading of classes. * - * @param string $class A class name. - * - * @return boolean Returns true if the class has been loaded + * @param string $class - A class name. + * @return boolean - Returns true if the class has been loaded */ public function autoload($class) { diff --git a/oauth/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php b/oauth/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php index 29c7171..6a167da 100644 --- a/oauth/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php +++ b/oauth/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php @@ -10,6 +10,19 @@ use OAuth2\ResponseInterface; */ interface ClientAssertionTypeInterface { + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return mixed + */ public function validateRequest(RequestInterface $request, ResponseInterface $response); + + /** + * Get the client id + * + * @return mixed + */ public function getClientId(); } diff --git a/oauth/OAuth2/ClientAssertionType/HttpBasic.php b/oauth/OAuth2/ClientAssertionType/HttpBasic.php index 0ecb7e1..ef61203 100644 --- a/oauth/OAuth2/ClientAssertionType/HttpBasic.php +++ b/oauth/OAuth2/ClientAssertionType/HttpBasic.php @@ -5,6 +5,7 @@ namespace OAuth2\ClientAssertionType; use OAuth2\Storage\ClientCredentialsInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; +use LogicException; /** * Validate a client via Http Basic authentication @@ -19,14 +20,16 @@ class HttpBasic implements ClientAssertionTypeInterface protected $config; /** - * @param OAuth2\Storage\ClientCredentialsInterface $clientStorage REQUIRED Storage class for retrieving client credentials information - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header - * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated - * ); - * + * Config array $config should look as follows: + * @code + * $config = array( + * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header + * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated + * ); + * @endcode + * + * @param ClientCredentialsInterface $storage Storage + * @param array $config Configuration options for the server */ public function __construct(ClientCredentialsInterface $storage, array $config = array()) { @@ -37,6 +40,14 @@ class HttpBasic implements ClientAssertionTypeInterface ), $config); } + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed + * @throws LogicException + */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { if (!$clientData = $this->getClientCredentials($request, $response)) { @@ -44,7 +55,7 @@ class HttpBasic implements ClientAssertionTypeInterface } if (!isset($clientData['client_id'])) { - throw new \LogicException('the clientData array must have "client_id" set'); + throw new LogicException('the clientData array must have "client_id" set'); } if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { @@ -70,6 +81,11 @@ class HttpBasic implements ClientAssertionTypeInterface return true; } + /** + * Get the client id + * + * @return mixed + */ public function getClientId() { return $this->clientData['client_id']; @@ -82,13 +98,14 @@ class HttpBasic implements ClientAssertionTypeInterface * According to the spec (draft 20), the client_id can be provided in * the Basic Authorization header (recommended) or via GET/POST. * - * @return - * A list containing the client identifier and password, for example + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array|null A list containing the client identifier and password, for example: * @code - * return array( - * "client_id" => CLIENT_ID, // REQUIRED the client id - * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) - * ); + * return array( + * "client_id" => CLIENT_ID, // REQUIRED the client id + * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) + * ); * @endcode * * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 @@ -108,7 +125,6 @@ class HttpBasic implements ClientAssertionTypeInterface * client_secret can be null if the client's password is an empty string * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 */ - return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); } } diff --git a/oauth/OAuth2/Controller/AuthorizeController.php b/oauth/OAuth2/Controller/AuthorizeController.php index 2c878a5..4bafb1d 100644 --- a/oauth/OAuth2/Controller/AuthorizeController.php +++ b/oauth/OAuth2/Controller/AuthorizeController.php @@ -7,37 +7,76 @@ use OAuth2\ScopeInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; use OAuth2\Scope; +use InvalidArgumentException; /** - * @see OAuth2\Controller\AuthorizeControllerInterface + * @see AuthorizeControllerInterface */ class AuthorizeController implements AuthorizeControllerInterface { + /** + * @var string + */ private $scope; + + /** + * @var int + */ private $state; + + /** + * @var mixed + */ private $client_id; + + /** + * @var string + */ private $redirect_uri; + + /** + * The response type + * + * @var string + */ private $response_type; + /** + * @var ClientInterface + */ protected $clientStorage; + + /** + * @var array + */ protected $responseTypes; + + /** + * @var array + */ protected $config; + + /** + * @var ScopeInterface + */ protected $scopeUtil; /** - * @param OAuth2\Storage\ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information - * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array - * keys are "code" and "token" - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type - * 'enforce_state' => true // if the controller should require the "state" parameter - * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter - * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses - * ); - * - * @param OAuth2\ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope + * Constructor + * + * @param ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information + * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array + * keys are "code" and "token" + * @param array $config OPTIONAL Configuration options for the server: + * @param ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope + * @code + * $config = array( + * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type + * 'enforce_state' => true // if the controller should require the "state" parameter + * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter + * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses + * ); + * @endcode */ public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null) { @@ -56,11 +95,20 @@ class AuthorizeController implements AuthorizeControllerInterface $this->scopeUtil = $scopeUtil; } + /** + * Handle the authorization request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param boolean $is_authorized + * @param mixed $user_id + * @return mixed|void + * @throws InvalidArgumentException + */ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) { - if (!is_bool($is_authorized)) { - throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.'); + throw new InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.'); } // We repeat this, because we need to re-validate. The request could be POSTed @@ -102,6 +150,14 @@ class AuthorizeController implements AuthorizeControllerInterface $response->setRedirect($this->config['redirect_status_code'], $uri); } + /** + * Set not authorized response + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param string $redirect_uri + * @param mixed $user_id + */ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null) { $error = 'access_denied'; @@ -109,9 +165,16 @@ class AuthorizeController implements AuthorizeControllerInterface $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message); } - /* + /** * We have made this protected so this class can be extended to add/modify * these parameters + * + * @TODO: add dependency injection for the parameters in this method + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param mixed $user_id + * @return array */ protected function buildAuthorizeParameters($request, $response, $user_id) { @@ -128,6 +191,8 @@ class AuthorizeController implements AuthorizeControllerInterface } /** + * Validate the OAuth request + * * @param RequestInterface $request * @param ResponseInterface $response * @return bool @@ -187,7 +252,7 @@ class AuthorizeController implements AuthorizeControllerInterface $redirect_uri = $registered_redirect_uri; } - // Select the redirect URI + // Select the response type $response_type = $request->query('response_type', $request->request('response_type')); // for multiple-valued response types - make them alphabetical @@ -282,10 +347,10 @@ class AuthorizeController implements AuthorizeControllerInterface /** * Build the absolute URI based on supplied URI and parameters. * - * @param $uri An absolute URI. - * @param $params Parameters to be append as GET. + * @param string $uri An absolute URI. + * @param array $params Parameters to be append as GET. * - * @return + * @return string * An absolute URI with supplied parameters. * * @ingroup oauth2_section_4 @@ -303,9 +368,9 @@ class AuthorizeController implements AuthorizeControllerInterface } } - // Put humpty dumpty back together + // Put the uri back together return - ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") + ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") . ((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "") . ((isset($parse_url["host"])) ? $parse_url["host"] : "") @@ -327,10 +392,10 @@ class AuthorizeController implements AuthorizeControllerInterface /** * Internal method for validating redirect URI supplied * - * @param string $inputUri The submitted URI to be validated + * @param string $inputUri The submitted URI to be validated * @param string $registeredUriString The allowed URI(s) to validate against. Can be a space-delimited string of URIs to * allow for multiple URIs - * + * @return bool * @see http://tools.ietf.org/html/rfc6749#section-3.1.2 */ protected function validateRedirectUri($inputUri, $registeredUriString) @@ -364,29 +429,50 @@ class AuthorizeController implements AuthorizeControllerInterface } /** - * Convenience methods to access the parameters derived from the validated request + * Convenience method to access the scope + * + * @return string */ - public function getScope() { return $this->scope; } + /** + * Convenience method to access the state + * + * @return int + */ public function getState() { return $this->state; } + /** + * Convenience method to access the client id + * + * @return mixed + */ public function getClientId() { return $this->client_id; } + /** + * Convenience method to access the redirect url + * + * @return string + */ public function getRedirectUri() { return $this->redirect_uri; } + /** + * Convenience method to access the response type + * + * @return string + */ public function getResponseType() { return $this->response_type; diff --git a/oauth/OAuth2/Controller/AuthorizeControllerInterface.php b/oauth/OAuth2/Controller/AuthorizeControllerInterface.php index fa07ae8..f758f97 100644 --- a/oauth/OAuth2/Controller/AuthorizeControllerInterface.php +++ b/oauth/OAuth2/Controller/AuthorizeControllerInterface.php @@ -11,17 +11,18 @@ use OAuth2\ResponseInterface; * authorization directly, this controller ensures the request is valid, but * requires the application to determine the value of $is_authorized * - * ex: - * > $user_id = $this->somehowDetermineUserId(); - * > $is_authorized = $this->somehowDetermineUserAuthorization(); - * > $response = new OAuth2\Response(); - * > $authorizeController->handleAuthorizeRequest( - * > OAuth2\Request::createFromGlobals(), - * > $response, - * > $is_authorized, - * > $user_id); - * > $response->send(); - * + * @code + * $user_id = $this->somehowDetermineUserId(); + * $is_authorized = $this->somehowDetermineUserAuthorization(); + * $response = new OAuth2\Response(); + * $authorizeController->handleAuthorizeRequest( + * OAuth2\Request::createFromGlobals(), + * $response, + * $is_authorized, + * $user_id + * ); + * $response->send(); + * @endcode */ interface AuthorizeControllerInterface { @@ -37,7 +38,21 @@ interface AuthorizeControllerInterface const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code'; const RESPONSE_TYPE_ACCESS_TOKEN = 'token'; + /** + * Handle the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param $is_authorized + * @param null $user_id + * @return mixed + */ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null); + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response); } diff --git a/oauth/OAuth2/Controller/ResourceController.php b/oauth/OAuth2/Controller/ResourceController.php index f3530b2..1e294ec 100644 --- a/oauth/OAuth2/Controller/ResourceController.php +++ b/oauth/OAuth2/Controller/ResourceController.php @@ -10,17 +10,43 @@ use OAuth2\ResponseInterface; use OAuth2\Scope; /** - * @see OAuth2\Controller\ResourceControllerInterface + * @see ResourceControllerInterface */ class ResourceController implements ResourceControllerInterface { + /** + * @var array + */ private $token; + /** + * @var TokenTypeInterface + */ protected $tokenType; + + /** + * @var AccessTokenInterface + */ protected $tokenStorage; + + /** + * @var array + */ protected $config; + + /** + * @var ScopeInterface + */ protected $scopeUtil; + /** + * Constructor + * + * @param TokenTypeInterface $tokenType + * @param AccessTokenInterface $tokenStorage + * @param array $config + * @param ScopeInterface $scopeUtil + */ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null) { $this->tokenType = $tokenType; @@ -36,6 +62,14 @@ class ResourceController implements ResourceControllerInterface $this->scopeUtil = $scopeUtil; } + /** + * Verify the resource request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param null $scope + * @return bool + */ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null) { $token = $this->getAccessTokenData($request, $response); @@ -71,6 +105,13 @@ class ResourceController implements ResourceControllerInterface return (bool) $token; } + /** + * Get access token data. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array|null + */ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response) { // Get the token parameter @@ -85,6 +126,7 @@ class ResourceController implements ResourceControllerInterface } elseif (time() > $token["expires"]) { $response->setError(401, 'invalid_token', 'The access token provided has expired'); } else { + // Get associated id for user. $token["assoc_id"]=$this->tokenStorage->getUsersID($token["user_id"]); return $token; } @@ -104,7 +146,11 @@ class ResourceController implements ResourceControllerInterface return null; } - // convenience method to allow retrieval of the token + /** + * convenience method to allow retrieval of the token. + * + * @return array + */ public function getToken() { return $this->token; diff --git a/oauth/OAuth2/Controller/ResourceControllerInterface.php b/oauth/OAuth2/Controller/ResourceControllerInterface.php index 6114219..0e847ca 100644 --- a/oauth/OAuth2/Controller/ResourceControllerInterface.php +++ b/oauth/OAuth2/Controller/ResourceControllerInterface.php @@ -10,17 +10,32 @@ use OAuth2\ResponseInterface; * call verifyResourceRequest in order to determine if the request * contains a valid token. * - * ex: - * > if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { - * > $response->send(); // authorization failed - * > die(); - * > } - * > return json_encode($resource); // valid token! Send the stuff! - * + * @code + * if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { + * $response->send(); // authorization failed + * die(); + * } + * return json_encode($resource); // valid token! Send the stuff! + * @endcode */ interface ResourceControllerInterface { + /** + * Verify the resource request + * + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @param string $scope + * @return mixed + */ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null); + /** + * Get access token data. + * + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @return mixed + */ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response); } diff --git a/oauth/OAuth2/Controller/TokenController.php b/oauth/OAuth2/Controller/TokenController.php index 5d2d731..7fdaf85 100644 --- a/oauth/OAuth2/Controller/TokenController.php +++ b/oauth/OAuth2/Controller/TokenController.php @@ -10,9 +10,12 @@ use OAuth2\Scope; use OAuth2\Storage\ClientInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; +use InvalidArgumentException; +use LogicException; +use RuntimeException; /** - * @see \OAuth2\Controller\TokenControllerInterface + * @see TokenControllerInterface */ class TokenController implements TokenControllerInterface { @@ -22,7 +25,7 @@ class TokenController implements TokenControllerInterface protected $accessToken; /** - * @var array + * @var array */ protected $grantTypes; @@ -32,7 +35,7 @@ class TokenController implements TokenControllerInterface protected $clientAssertionType; /** - * @var Scope|ScopeInterface + * @var ScopeInterface */ protected $scopeUtil; @@ -41,12 +44,22 @@ class TokenController implements TokenControllerInterface */ protected $clientStorage; + /** + * Constructor + * + * @param AccessTokenInterface $accessToken + * @param ClientInterface $clientStorage + * @param array $grantTypes + * @param ClientAssertionTypeInterface $clientAssertionType + * @param ScopeInterface $scopeUtil + * @throws InvalidArgumentException + */ public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null) { if (is_null($clientAssertionType)) { foreach ($grantTypes as $grantType) { if (!$grantType instanceof ClientAssertionTypeInterface) { - throw new \InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface'); + throw new InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface'); } } } @@ -63,6 +76,12 @@ class TokenController implements TokenControllerInterface $this->scopeUtil = $scopeUtil; } + /** + * Handle the token request. + * + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object + */ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response) { if ($token = $this->grantAccessToken($request, $response)) { @@ -83,8 +102,10 @@ class TokenController implements TokenControllerInterface * This would be called from the "/token" endpoint as defined in the spec. * You can call your endpoint whatever you want. * - * @param RequestInterface $request Request object to grant access token - * @param ResponseInterface $response + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object + * + * @return bool|null|array * * @throws \InvalidArgumentException * @throws \LogicException @@ -97,9 +118,15 @@ class TokenController implements TokenControllerInterface */ public function grantAccessToken(RequestInterface $request, ResponseInterface $response) { - if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + if (strtolower($request->server('REQUEST_METHOD')) === 'options') { + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); + + return null; + } + + if (strtolower($request->server('REQUEST_METHOD')) !== 'post') { $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2'); - $response->addHttpHeaders(array('Allow' => 'POST')); + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); return null; } @@ -121,6 +148,7 @@ class TokenController implements TokenControllerInterface return null; } + /** @var GrantTypeInterface $grantType */ $grantType = $this->grantTypes[$grantTypeIdentifier]; /** @@ -128,8 +156,8 @@ class TokenController implements TokenControllerInterface * ClientAssertionTypes allow for grant types which also assert the client data * in which case ClientAssertion is handled in the validateRequest method * - * @see OAuth2\GrantType\JWTBearer - * @see OAuth2\GrantType\ClientCredentials + * @see \OAuth2\GrantType\JWTBearer + * @see \OAuth2\GrantType\ClientCredentials */ if (!$grantType instanceof ClientAssertionTypeInterface) { if (!$this->clientAssertionType->validateRequest($request, $response)) { @@ -178,7 +206,6 @@ class TokenController implements TokenControllerInterface * * @see http://tools.ietf.org/html/rfc6749#section-3.3 */ - $requestedScope = $this->scopeUtil->getScopeFromRequest($request); $availableScope = $grantType->getScope(); @@ -225,20 +252,24 @@ class TokenController implements TokenControllerInterface } /** - * addGrantType + * Add grant type * - * @param GrantTypeInterface $grantType the grant type to add for the specified identifier - * @param string $identifier a string passed in as "grant_type" in the response that will call this grantType + * @param GrantTypeInterface $grantType - the grant type to add for the specified identifier + * @param string|null $identifier - a string passed in as "grant_type" in the response that will call this grantType */ public function addGrantType(GrantTypeInterface $grantType, $identifier = null) { if (is_null($identifier) || is_numeric($identifier)) { - $identifier = $grantType->getQuerystringIdentifier(); + $identifier = $grantType->getQueryStringIdentifier(); } $this->grantTypes[$identifier] = $grantType; } + /** + * @param RequestInterface $request + * @param ResponseInterface $response + */ public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response) { if ($this->revokeToken($request, $response)) { @@ -257,13 +288,20 @@ class TokenController implements TokenControllerInterface * * @param RequestInterface $request * @param ResponseInterface $response + * @throws RuntimeException * @return bool|null */ public function revokeToken(RequestInterface $request, ResponseInterface $response) { - if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + if (strtolower($request->server('REQUEST_METHOD')) === 'options') { + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); + + return null; + } + + if (strtolower($request->server('REQUEST_METHOD')) !== 'post') { $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2'); - $response->addHttpHeaders(array('Allow' => 'POST')); + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); return null; } @@ -285,7 +323,7 @@ class TokenController implements TokenControllerInterface // @todo remove this check for v2.0 if (!method_exists($this->accessToken, 'revokeToken')) { $class = get_class($this->accessToken); - throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method"); + throw new RuntimeException("AccessToken {$class} does not implement required revokeToken method"); } $this->accessToken->revokeToken($token, $token_type_hint); diff --git a/oauth/OAuth2/Controller/TokenControllerInterface.php b/oauth/OAuth2/Controller/TokenControllerInterface.php index 72d7257..2f83ce4 100644 --- a/oauth/OAuth2/Controller/TokenControllerInterface.php +++ b/oauth/OAuth2/Controller/TokenControllerInterface.php @@ -10,23 +10,30 @@ use OAuth2\ResponseInterface; * it is called to handle all grant types the application supports. * It also validates the client's credentials * - * ex: - * > $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); - * > $response->send(); - * + * @code + * $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); + * $response->send(); + * @endcode */ interface TokenControllerInterface { /** - * handleTokenRequest - * - * @param $request - * OAuth2\RequestInterface - The current http request - * @param $response - * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data + * Handle the token request * + * @param RequestInterface $request - The current http request + * @param ResponseInterface $response - An instance of OAuth2\ResponseInterface to contain the response data */ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response); + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * You can call your endpoint whatever you want. + * + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object + * + * @return mixed + */ public function grantAccessToken(RequestInterface $request, ResponseInterface $response); } diff --git a/oauth/OAuth2/Encryption/EncryptionInterface.php b/oauth/OAuth2/Encryption/EncryptionInterface.php new file mode 100644 index 0000000..8dc720a --- /dev/null +++ b/oauth/OAuth2/Encryption/EncryptionInterface.php @@ -0,0 +1,34 @@ + + */ +class FirebaseJwt implements EncryptionInterface +{ + public function __construct() + { + if (!class_exists('\JWT')) { + throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"'); + } + } + + public function encode($payload, $key, $alg = 'HS256', $keyId = null) + { + return \JWT::encode($payload, $key, $alg, $keyId); + } + + public function decode($jwt, $key = null, $allowedAlgorithms = null) + { + try { + + //Maintain BC: Do not verify if no algorithms are passed in. + if (!$allowedAlgorithms) { + $key = null; + } + + return (array)\JWT::decode($jwt, $key, $allowedAlgorithms); + } catch (\Exception $e) { + return false; + } + } + + public function urlSafeB64Encode($data) + { + return \JWT::urlsafeB64Encode($data); + } + + public function urlSafeB64Decode($b64) + { + return \JWT::urlsafeB64Decode($b64); + } +} diff --git a/oauth/OAuth2/Encryption/Jwt.php b/oauth/OAuth2/Encryption/Jwt.php new file mode 100644 index 0000000..c258b8f --- /dev/null +++ b/oauth/OAuth2/Encryption/Jwt.php @@ -0,0 +1,223 @@ +generateJwtHeader($payload, $algo); + + $segments = array( + $this->urlSafeB64Encode(json_encode($header)), + $this->urlSafeB64Encode(json_encode($payload)) + ); + + $signing_input = implode('.', $segments); + + $signature = $this->sign($signing_input, $key, $algo); + $segments[] = $this->urlsafeB64Encode($signature); + + return implode('.', $segments); + } + + /** + * @param string $jwt + * @param null $key + * @param array|bool $allowedAlgorithms + * @return bool|mixed + */ + public function decode($jwt, $key = null, $allowedAlgorithms = true) + { + if (!strpos($jwt, '.')) { + return false; + } + + $tks = explode('.', $jwt); + + if (count($tks) != 3) { + return false; + } + + list($headb64, $payloadb64, $cryptob64) = $tks; + + if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) { + return false; + } + + if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) { + return false; + } + + $sig = $this->urlSafeB64Decode($cryptob64); + + if ((bool) $allowedAlgorithms) { + if (!isset($header['alg'])) { + return false; + } + + // check if bool arg supplied here to maintain BC + if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) { + return false; + } + + if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) { + return false; + } + } + + return $payload; + } + + /** + * @param $signature + * @param $input + * @param $key + * @param string $algo + * @return bool + * @throws InvalidArgumentException + */ + private function verifySignature($signature, $input, $key, $algo = 'HS256') + { + // use constants when possible, for HipHop support + switch ($algo) { + case'HS256': + case'HS384': + case'HS512': + return $this->hash_equals( + $this->sign($input, $key, $algo), + $signature + ); + + case 'RS256': + return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1; + + case 'RS384': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1; + + case 'RS512': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1; + + default: + throw new InvalidArgumentException("Unsupported or invalid signing algorithm."); + } + } + + /** + * @param $input + * @param $key + * @param string $algo + * @return string + * @throws Exception + */ + private function sign($input, $key, $algo = 'HS256') + { + switch ($algo) { + case 'HS256': + return hash_hmac('sha256', $input, $key, true); + + case 'HS384': + return hash_hmac('sha384', $input, $key, true); + + case 'HS512': + return hash_hmac('sha512', $input, $key, true); + + case 'RS256': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256'); + + case 'RS384': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384'); + + case 'RS512': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512'); + + default: + throw new Exception("Unsupported or invalid signing algorithm."); + } + } + + /** + * @param $input + * @param $key + * @param string $algo + * @return mixed + * @throws Exception + */ + private function generateRSASignature($input, $key, $algo) + { + if (!openssl_sign($input, $signature, $key, $algo)) { + throw new Exception("Unable to sign data."); + } + + return $signature; + } + + /** + * @param string $data + * @return string + */ + public function urlSafeB64Encode($data) + { + $b64 = base64_encode($data); + $b64 = str_replace(array('+', '/', "\r", "\n", '='), + array('-', '_'), + $b64); + + return $b64; + } + + /** + * @param string $b64 + * @return mixed|string + */ + public function urlSafeB64Decode($b64) + { + $b64 = str_replace(array('-', '_'), + array('+', '/'), + $b64); + + return base64_decode($b64); + } + + /** + * Override to create a custom header + */ + protected function generateJwtHeader($payload, $algorithm) + { + return array( + 'typ' => 'JWT', + 'alg' => $algorithm, + ); + } + + /** + * @param string $a + * @param string $b + * @return bool + */ + protected function hash_equals($a, $b) + { + if (function_exists('hash_equals')) { + return hash_equals($a, $b); + } + $diff = strlen($a) ^ strlen($b); + for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { + $diff |= ord($a[$i]) ^ ord($b[$i]); + } + + return $diff === 0; + } +} \ No newline at end of file diff --git a/oauth/OAuth2/GrantType/AuthorizationCode.php b/oauth/OAuth2/GrantType/AuthorizationCode.php index cae9f78..784f6b3 100644 --- a/oauth/OAuth2/GrantType/AuthorizationCode.php +++ b/oauth/OAuth2/GrantType/AuthorizationCode.php @@ -6,29 +6,47 @@ use OAuth2\Storage\AuthorizationCodeInterface; use OAuth2\ResponseType\AccessTokenInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; +use Exception; /** - * * @author Brent Shaffer */ class AuthorizationCode implements GrantTypeInterface { + /** + * @var AuthorizationCodeInterface + */ protected $storage; + + /** + * @var array + */ protected $authCode; /** - * @param \OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information + * @param AuthorizationCodeInterface $storage - REQUIRED Storage class for retrieving authorization code information */ public function __construct(AuthorizationCodeInterface $storage) { $this->storage = $storage; } - public function getQuerystringIdentifier() + /** + * @return string + */ + public function getQueryStringIdentifier() { return 'authorization_code'; } + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + * @throws Exception + */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { if (!$request->request('code')) { @@ -75,21 +93,45 @@ class AuthorizationCode implements GrantTypeInterface return true; } + /** + * Get the client id + * + * @return mixed + */ public function getClientId() { return $this->authCode['client_id']; } + /** + * Get the scope + * + * @return string + */ public function getScope() { return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; } + /** + * Get the user id + * + * @return mixed + */ public function getUserId() { return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null; } + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { $token = $accessToken->createAccessToken($client_id, $user_id, $scope); diff --git a/oauth/OAuth2/GrantType/ClientCredentials.php b/oauth/OAuth2/GrantType/ClientCredentials.php new file mode 100644 index 0000000..e135c2d --- /dev/null +++ b/oauth/OAuth2/GrantType/ClientCredentials.php @@ -0,0 +1,98 @@ + + * + * @see HttpBasic + */ +class ClientCredentials extends HttpBasic implements GrantTypeInterface +{ + /** + * @var array + */ + private $clientData; + + /** + * @param ClientCredentialsInterface $storage + * @param array $config + */ + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + /** + * The client credentials grant type MUST only be used by confidential clients + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4 + */ + $config['allow_public_clients'] = false; + + parent::__construct($storage, $config); + } + + /** + * Get query string identifier + * + * @return string + */ + public function getQueryStringIdentifier() + { + return 'client_credentials'; + } + + /** + * Get scope + * + * @return string|null + */ + public function getScope() + { + $this->loadClientData(); + + return isset($this->clientData['scope']) ? $this->clientData['scope'] : null; + } + + /** + * Get user id + * + * @return mixed + */ + public function getUserId() + { + $this->loadClientData(); + + return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null; + } + + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + /** + * Client Credentials Grant does NOT include a refresh token + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 + */ + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } + + private function loadClientData() + { + if (!$this->clientData) { + $this->clientData = $this->storage->getClientDetails($this->getClientId()); + } + } +} diff --git a/oauth/OAuth2/GrantType/GrantTypeInterface.php b/oauth/OAuth2/GrantType/GrantTypeInterface.php index 98489e9..f45786f 100644 --- a/oauth/OAuth2/GrantType/GrantTypeInterface.php +++ b/oauth/OAuth2/GrantType/GrantTypeInterface.php @@ -11,10 +11,49 @@ use OAuth2\ResponseInterface; */ interface GrantTypeInterface { - public function getQuerystringIdentifier(); + /** + * Get query string identifier + * + * @return string + */ + public function getQueryStringIdentifier(); + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return mixed + */ public function validateRequest(RequestInterface $request, ResponseInterface $response); + + /** + * Get client id + * + * @return mixed + */ public function getClientId(); + + /** + * Get user id + * + * @return mixed + */ public function getUserId(); + + /** + * Get scope + * + * @return string|null + */ public function getScope(); + + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope); } diff --git a/oauth/OAuth2/GrantType/JwtBearer.php b/oauth/OAuth2/GrantType/JwtBearer.php new file mode 100644 index 0000000..62c1efa --- /dev/null +++ b/oauth/OAuth2/GrantType/JwtBearer.php @@ -0,0 +1,247 @@ + + */ +class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface +{ + private $jwt; + + protected $storage; + protected $audience; + protected $jwtUtil; + protected $allowedAlgorithms; + + /** + * Creates an instance of the JWT bearer grant type. + * + * @param JwtBearerInterface $storage - A valid storage interface that implements storage hooks for the JWT + * bearer grant type. + * @param string $audience - The audience to validate the token against. This is usually the full + * URI of the OAuth token requests endpoint. + * @param EncryptionInterface|JWT $jwtUtil - OPTONAL The class used to decode, encode and verify JWTs. + * @param array $config + */ + public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array()) + { + $this->storage = $storage; + $this->audience = $audience; + + if (is_null($jwtUtil)) { + $jwtUtil = new Jwt(); + } + + $this->config = array_merge(array( + 'allowed_algorithms' => array('RS256', 'RS384', 'RS512') + ), $config); + + $this->jwtUtil = $jwtUtil; + + $this->allowedAlgorithms = $this->config['allowed_algorithms']; + } + + /** + * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant. + * + * @return string - The string identifier for grant_type. + * + * @see GrantTypeInterface::getQueryStringIdentifier() + */ + public function getQueryStringIdentifier() + { + return 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + } + + /** + * Validates the data from the decoded JWT. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed|null TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned.@see GrantTypeInterface::getTokenData() + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("assertion")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required'); + + return null; + } + + // Store the undecoded JWT for later use + $undecodedJWT = $request->request('assertion'); + + // Decode the JWT + $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false); + + if (!$jwt) { + $response->setError(400, 'invalid_request', "JWT is malformed"); + + return null; + } + + // ensure these properties contain a value + // @todo: throw malformed error for missing properties + $jwt = array_merge(array( + 'scope' => null, + 'iss' => null, + 'sub' => null, + 'aud' => null, + 'exp' => null, + 'nbf' => null, + 'iat' => null, + 'jti' => null, + 'typ' => null, + ), $jwt); + + if (!isset($jwt['iss'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided"); + + return null; + } + + if (!isset($jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided"); + + return null; + } + + if (!isset($jwt['exp'])) { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present"); + + return null; + } + + // Check expiration + if (ctype_digit($jwt['exp'])) { + if ($jwt['exp'] <= time()) { + $response->setError(400, 'invalid_grant', "JWT has expired"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp"); + + return null; + } + + // Check the not before time + if ($notBefore = $jwt['nbf']) { + if (ctype_digit($notBefore)) { + if ($notBefore > time()) { + $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp"); + + return null; + } + } + + // Check the audience if required to match + if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) { + $response->setError(400, 'invalid_grant', "Invalid audience (aud)"); + + return null; + } + + // Check the jti (nonce) + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-13#section-4.1.7 + if (isset($jwt['jti'])) { + $jti = $this->storage->getJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + + //Reject if jti is used and jwt is still valid (exp parameter has not expired). + if ($jti && $jti['expires'] > time()) { + $response->setError(400, 'invalid_grant', "JSON Token Identifier (jti) has already been used"); + + return null; + } else { + $this->storage->setJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + } + } + + // Get the iss's public key + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1 + if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided"); + + return null; + } + + // Verify the JWT + if (!$this->jwtUtil->decode($undecodedJWT, $key, $this->allowedAlgorithms)) { + $response->setError(400, 'invalid_grant', "JWT failed signature verification"); + + return null; + } + + $this->jwt = $jwt; + + return true; + } + + /** + * Get client id + * + * @return mixed + */ + public function getClientId() + { + return $this->jwt['iss']; + } + + /** + * Get user id + * + * @return mixed + */ + public function getUserId() + { + return $this->jwt['sub']; + } + + /** + * Get scope + * + * @return null + */ + public function getScope() + { + return null; + } + + /** + * Creates an access token that is NOT associated with a refresh token. + * If a subject (sub) the name of the user/account we are accessing data on behalf of. + * + * @see GrantTypeInterface::createAccessToken() + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } +} diff --git a/oauth/OAuth2/GrantType/RefreshToken.php b/oauth/OAuth2/GrantType/RefreshToken.php index e553852..75c611f 100644 --- a/oauth/OAuth2/GrantType/RefreshToken.php +++ b/oauth/OAuth2/GrantType/RefreshToken.php @@ -8,25 +8,34 @@ use OAuth2\RequestInterface; use OAuth2\ResponseInterface; /** - * * @author Brent Shaffer */ class RefreshToken implements GrantTypeInterface { + /** + * @var array + */ private $refreshToken; + /** + * @var RefreshTokenInterface + */ protected $storage; + + /** + * @var array + */ protected $config; /** - * @param OAuth2\Storage\RefreshTokenInterface $storage REQUIRED Storage class for retrieving refresh token information - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request - * 'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using - * ); - * + * @param RefreshTokenInterface $storage - REQUIRED Storage class for retrieving refresh token information + * @param array $config - OPTIONAL Configuration options for the server + * @code + * $config = array( + * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request + * 'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using + * ); + * @endcode */ public function __construct(RefreshTokenInterface $storage, $config = array()) { @@ -45,11 +54,21 @@ class RefreshToken implements GrantTypeInterface $this->storage = $storage; } - public function getQuerystringIdentifier() + /** + * @return string + */ + public function getQueryStringIdentifier() { return 'refresh_token'; } + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed|null + */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { if (!$request->request("refresh_token")) { @@ -76,21 +95,45 @@ class RefreshToken implements GrantTypeInterface return true; } + /** + * Get client id + * + * @return mixed + */ public function getClientId() { return $this->refreshToken['client_id']; } + /** + * Get user id + * + * @return mixed|null + */ public function getUserId() { return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null; } + /** + * Get scope + * + * @return null|string + */ public function getScope() { return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null; } + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { /* diff --git a/oauth/OAuth2/GrantType/UserCredentials.php b/oauth/OAuth2/GrantType/UserCredentials.php new file mode 100644 index 0000000..b10c2dd --- /dev/null +++ b/oauth/OAuth2/GrantType/UserCredentials.php @@ -0,0 +1,123 @@ + + */ +class UserCredentials implements GrantTypeInterface +{ + /** + * @var array + */ + private $userInfo; + + /** + * @var UserCredentialsInterface + */ + protected $storage; + + /** + * @param UserCredentialsInterface $storage - REQUIRED Storage class for retrieving user credentials information + */ + public function __construct(UserCredentialsInterface $storage) + { + $this->storage = $storage; + } + + /** + * @return string + */ + public function getQueryStringIdentifier() + { + return 'password'; + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed|null + * + * @throws LogicException + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("password") || !$request->request("username")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); + + return null; + } + + if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) { + $response->setError(401, 'invalid_grant', 'Invalid username and password combination'); + + return null; + } + + $userInfo = $this->storage->getUserDetails($request->request("username")); + + if (empty($userInfo)) { + $response->setError(400, 'invalid_grant', 'Unable to retrieve user information'); + + return null; + } + + if (!isset($userInfo['user_id'])) { + throw new \LogicException("you must set the user_id on the array returned by getUserDetails"); + } + + $this->userInfo = $userInfo; + + return true; + } + + /** + * Get client id + * + * @return mixed|null + */ + public function getClientId() + { + return null; + } + + /** + * Get user id + * + * @return mixed + */ + public function getUserId() + { + return $this->userInfo['user_id']; + } + + /** + * Get scope + * + * @return null|string + */ + public function getScope() + { + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; + } + + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + return $accessToken->createAccessToken($client_id, $user_id, $scope); + } +} diff --git a/oauth/OAuth2/OpenID/Controller/AuthorizeController.php b/oauth/OAuth2/OpenID/Controller/AuthorizeController.php new file mode 100644 index 0000000..54c5f9a --- /dev/null +++ b/oauth/OAuth2/OpenID/Controller/AuthorizeController.php @@ -0,0 +1,135 @@ +query('prompt', 'consent'); + if ($prompt == 'none') { + if (is_null($user_id)) { + $error = 'login_required'; + $error_message = 'The user must log in'; + } else { + $error = 'interaction_required'; + $error_message = 'The user must grant access to your application'; + } + } else { + $error = 'consent_required'; + $error_message = 'The user denied access to your application'; + } + + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->getState(), $error, $error_message); + } + + /** + * @TODO: add dependency injection for the parameters in this method + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param mixed $user_id + * @return array + */ + protected function buildAuthorizeParameters($request, $response, $user_id) + { + if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) { + return; + } + + // Generate an id token if needed. + if ($this->needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { + $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce); + } + + // add the nonce to return with the redirect URI + $params['nonce'] = $this->nonce; + + return $params; + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) + { + if (!parent::validateAuthorizeRequest($request, $response)) { + return false; + } + + $nonce = $request->query('nonce'); + + // Validate required nonce for "id_token" and "id_token token" + if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) { + $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter'); + + return false; + } + + $this->nonce = $nonce; + + return true; + } + + /** + * Array of valid response types + * + * @return array + */ + protected function getValidResponseTypes() + { + return array( + self::RESPONSE_TYPE_ACCESS_TOKEN, + self::RESPONSE_TYPE_AUTHORIZATION_CODE, + self::RESPONSE_TYPE_ID_TOKEN, + self::RESPONSE_TYPE_ID_TOKEN_TOKEN, + self::RESPONSE_TYPE_CODE_ID_TOKEN, + ); + } + + /** + * Returns whether the current request needs to generate an id token. + * + * ID Tokens are a part of the OpenID Connect specification, so this + * method checks whether OpenID Connect is enabled in the server settings + * and whether the openid scope was requested. + * + * @param string $request_scope - A space-separated string of scopes. + * @return boolean - TRUE if an id token is needed, FALSE otherwise. + */ + public function needsIdToken($request_scope) + { + // see if the "openid" scope exists in the requested scope + return $this->scopeUtil->checkScope('openid', $request_scope); + } + + /** + * @return mixed + */ + public function getNonce() + { + return $this->nonce; + } +} diff --git a/oauth/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/oauth/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php new file mode 100644 index 0000000..b4967c3 --- /dev/null +++ b/oauth/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php @@ -0,0 +1,12 @@ +userClaimsStorage = $userClaimsStorage; + } + + /** + * Handle the user info request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$this->verifyResourceRequest($request, $response, 'openid')) { + return; + } + + $token = $this->getToken(); + $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']); + // The sub Claim MUST always be returned in the UserInfo Response. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + $claims += array( + 'sub' => $token['user_id'], + ); + $response->addParameters($claims); + } +} \ No newline at end of file diff --git a/oauth/OAuth2/OpenID/Controller/UserInfoControllerInterface.php b/oauth/OAuth2/OpenID/Controller/UserInfoControllerInterface.php new file mode 100644 index 0000000..88e9228 --- /dev/null +++ b/oauth/OAuth2/OpenID/Controller/UserInfoControllerInterface.php @@ -0,0 +1,30 @@ +handleUserInfoRequest( + * OAuth2\Request::createFromGlobals(), + * $response + * ); + * $response->send(); + * @endcode + */ +interface UserInfoControllerInterface +{ + /** + * Handle user info request + * + * @param RequestInterface $request + * @param ResponseInterface $response + */ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response); +} diff --git a/oauth/OAuth2/OpenID/GrantType/AuthorizationCode.php b/oauth/OAuth2/OpenID/GrantType/AuthorizationCode.php new file mode 100644 index 0000000..ee113a0 --- /dev/null +++ b/oauth/OAuth2/OpenID/GrantType/AuthorizationCode.php @@ -0,0 +1,41 @@ + + */ +class AuthorizationCode extends BaseAuthorizationCode +{ + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = true; + if (isset($this->authCode['id_token'])) { + // OpenID Connect requests include the refresh token only if the + // offline_access scope has been requested and granted. + $scopes = explode(' ', trim($scope)); + $includeRefreshToken = in_array('offline_access', $scopes); + } + + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + if (isset($this->authCode['id_token'])) { + $token['id_token'] = $this->authCode['id_token']; + } + + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; + } +} diff --git a/oauth/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/oauth/OAuth2/OpenID/ResponseType/AuthorizationCode.php new file mode 100644 index 0000000..b8ad41f --- /dev/null +++ b/oauth/OAuth2/OpenID/ResponseType/AuthorizationCode.php @@ -0,0 +1,66 @@ + + */ +class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface +{ + /** + * Constructor + * + * @param AuthorizationCodeStorageInterface $storage + * @param array $config + */ + public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) + { + parent::__construct($storage, $config); + } + + /** + * @param $params + * @param null $user_id + * @return array + */ + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null, 'id_token' => null); + + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']); + + if (isset($params['state'])) { + $result['query']['state'] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of the authorization code. + * + * @param mixed $client_id - Client identifier related to the authorization code + * @param mixed $user_id - User ID associated with the authorization code + * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. + * @param string $id_token - OPTIONAL The OpenID Connect id_token. + * + * @return string + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null) + { + $code = $this->generateAuthorizationCode(); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token); + + return $code; + } +} diff --git a/oauth/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php b/oauth/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php new file mode 100644 index 0000000..eb94ef0 --- /dev/null +++ b/oauth/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php @@ -0,0 +1,27 @@ + + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Handle the creation of the authorization code. + * + * @param mixed $client_id - Client identifier related to the authorization code + * @param mixed $user_id - User ID associated with the authorization code + * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. + * @param string $id_token - OPTIONAL The OpenID Connect id_token. + * @return string + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null); +} diff --git a/oauth/OAuth2/OpenID/ResponseType/CodeIdToken.php b/oauth/OAuth2/OpenID/ResponseType/CodeIdToken.php new file mode 100644 index 0000000..2696ada --- /dev/null +++ b/oauth/OAuth2/OpenID/ResponseType/CodeIdToken.php @@ -0,0 +1,40 @@ +authCode = $authCode; + $this->idToken = $idToken; + } + + /** + * @param array $params + * @param mixed $user_id + * @return mixed + */ + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->authCode->getAuthorizeResponse($params, $user_id); + $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id); + $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token']; + + return $result; + } +} diff --git a/oauth/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php b/oauth/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php new file mode 100644 index 0000000..629adcc --- /dev/null +++ b/oauth/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php @@ -0,0 +1,9 @@ +userClaimsStorage = $userClaimsStorage; + $this->publicKeyStorage = $publicKeyStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + + if (!isset($config['issuer'])) { + throw new LogicException('config parameter "issuer" must be set'); + } + $this->config = array_merge(array( + 'id_lifetime' => 3600, + ), $config); + } + + /** + * @param array $params + * @param null $userInfo + * @return array|mixed + */ + public function getAuthorizeResponse($params, $userInfo = null) + { + // build the URL to redirect to + $result = array('query' => array()); + $params += array('scope' => null, 'state' => null, 'nonce' => null); + + // create the id token. + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']); + + $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null); + $result["fragment"] = array('id_token' => $id_token); + if (isset($params['state'])) { + $result["fragment"]["state"] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Create id token + * + * @param string $client_id + * @param mixed $userInfo + * @param mixed $nonce + * @param mixed $userClaims + * @param mixed $access_token + * @return mixed|string + */ + public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null) + { + // pull auth_time from user info if supplied + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + + $token = array( + 'iss' => $this->config['issuer'], + 'sub' => $user_id, + 'aud' => $client_id, + 'iat' => time(), + 'exp' => time() + $this->config['id_lifetime'], + 'auth_time' => $auth_time, + ); + + if ($nonce) { + $token['nonce'] = $nonce; + } + + if ($userClaims) { + $token += $userClaims; + } + + if ($access_token) { + $token['at_hash'] = $this->createAtHash($access_token, $client_id); + } + + return $this->encodeToken($token, $client_id); + } + + /** + * @param $access_token + * @param null $client_id + * @return mixed|string + */ + protected function createAtHash($access_token, $client_id = null) + { + // maps HS256 and RS256 to sha256, etc. + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + $hash_algorithm = 'sha' . substr($algorithm, 2); + $hash = hash($hash_algorithm, $access_token, true); + $at_hash = substr($hash, 0, strlen($hash) / 2); + + return $this->encryptionUtil->urlSafeB64Encode($at_hash); + } + + /** + * @param array $token + * @param null $client_id + * @return mixed|string + */ + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } + + /** + * @param $userInfo + * @return array + * @throws LogicException + */ + private function getUserIdAndAuthTime($userInfo) + { + $auth_time = null; + + // support an array for user_id / auth_time + if (is_array($userInfo)) { + if (!isset($userInfo['user_id'])) { + throw new LogicException('if $user_id argument is an array, user_id index must be set'); + } + + $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null; + $user_id = $userInfo['user_id']; + } else { + $user_id = $userInfo; + } + + if (is_null($auth_time)) { + $auth_time = time(); + } + + // userInfo is a scalar, and so this is the $user_id. Auth Time is null + return array($user_id, $auth_time); + } +} diff --git a/oauth/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/oauth/OAuth2/OpenID/ResponseType/IdTokenInterface.php new file mode 100644 index 0000000..226a3bc --- /dev/null +++ b/oauth/OAuth2/OpenID/ResponseType/IdTokenInterface.php @@ -0,0 +1,30 @@ +accessToken = $accessToken; + $this->idToken = $idToken; + } + + /** + * @param array $params + * @param mixed $user_id + * @return mixed + */ + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->accessToken->getAuthorizeResponse($params, $user_id); + $access_token = $result[1]['fragment']['access_token']; + $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token); + $result[1]['fragment']['id_token'] = $id_token; + + return $result; + } +} diff --git a/oauth/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php b/oauth/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php new file mode 100644 index 0000000..ac13e20 --- /dev/null +++ b/oauth/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php @@ -0,0 +1,9 @@ + + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param string $code - authorization code to be stored. + * @param mixed $client_id - client identifier to be stored. + * @param mixed $user_id - user identifier to be stored. + * @param string $redirect_uri - redirect URI(s) to be stored in a space-separated string. + * @param int $expires - expiration to be stored as a Unix timestamp. + * @param string $scope - OPTIONAL scopes to be stored in space-separated string. + * @param string $id_token - OPTIONAL the OpenID Connect id_token. + * + * @ingroup oauth2_section_4 + */ + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null); +} diff --git a/oauth/OAuth2/OpenID/Storage/UserClaimsInterface.php b/oauth/OAuth2/OpenID/Storage/UserClaimsInterface.php new file mode 100644 index 0000000..9c5e7c8 --- /dev/null +++ b/oauth/OAuth2/OpenID/Storage/UserClaimsInterface.php @@ -0,0 +1,35 @@ + value format. + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + */ + public function getUserClaims($user_id, $scope); +} diff --git a/oauth/OAuth2/Request.php b/oauth/OAuth2/Request.php index c92cee8..f547bf6 100644 --- a/oauth/OAuth2/Request.php +++ b/oauth/OAuth2/Request.php @@ -2,6 +2,8 @@ namespace OAuth2; +use LogicException; + /** * OAuth2\Request * This class is taken from the Symfony2 Framework and is part of the Symfony package. @@ -21,13 +23,14 @@ class Request implements RequestInterface /** * Constructor. * - * @param array $query The GET parameters - * @param array $request The POST parameters - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) - * @param array $cookies The COOKIE parameters - * @param array $files The FILES parameters - * @param array $server The SERVER parameters - * @param string $content The raw body data + * @param array $query - The GET parameters + * @param array $request - The POST parameters + * @param array $attributes - The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies - The COOKIE parameters + * @param array $files - The FILES parameters + * @param array $server - The SERVER parameters + * @param string $content - The raw body data + * @param array $headers - The headers * * @api */ @@ -41,13 +44,14 @@ class Request implements RequestInterface * * This method also re-initializes all properties. * - * @param array $query The GET parameters - * @param array $request The POST parameters - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) - * @param array $cookies The COOKIE parameters - * @param array $files The FILES parameters - * @param array $server The SERVER parameters - * @param string $content The raw body data + * @param array $query - The GET parameters + * @param array $request - The POST parameters + * @param array $attributes - The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies - The COOKIE parameters + * @param array $files - The FILES parameters + * @param array $server - The SERVER parameters + * @param string $content - The raw body data + * @param array $headers - The headers * * @api */ @@ -60,24 +64,49 @@ class Request implements RequestInterface $this->files = $files; $this->server = $server; $this->content = $content; - $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers; + + if ($headers === null) { + $headers = array(); + } + + $this->headers = $headers + $this->getHeadersFromServer($this->server); } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function query($name, $default = null) { return isset($this->query[$name]) ? $this->query[$name] : $default; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function request($name, $default = null) { return isset($this->request[$name]) ? $this->request[$name] : $default; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function server($name, $default = null) { return isset($this->server[$name]) ? $this->server[$name] : $default; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function headers($name, $default = null) { $headers = array_change_key_case($this->headers); @@ -86,6 +115,9 @@ class Request implements RequestInterface return isset($headers[$name]) ? $headers[$name] : $default; } + /** + * @return array + */ public function getAllQueryParameters() { return $this->query; @@ -94,14 +126,15 @@ class Request implements RequestInterface /** * Returns the request body content. * - * @param Boolean $asResource If true, a resource will be returned + * @param boolean $asResource - If true, a resource will be returned + * @return string|resource - The request body content or a resource to read the body stream. * - * @return string|resource The request body content or a resource to read the body stream. + * @throws LogicException */ public function getContent($asResource = false) { if (false === $this->content || (true === $asResource && null !== $this->content)) { - throw new \LogicException('getContent() can only be called once when using the resource return type.'); + throw new LogicException('getContent() can only be called once when using the resource return type.'); } if (true === $asResource) { @@ -117,6 +150,10 @@ class Request implements RequestInterface return $this->content; } + /** + * @param array $server + * @return array + */ private function getHeadersFromServer($server) { $headers = array(); @@ -185,13 +222,15 @@ class Request implements RequestInterface /** * Creates a new request with values from PHP's super globals. * - * @return Request A new request + * @return Request - A new request * * @api */ public static function createFromGlobals() { $class = get_called_class(); + + /** @var Request $request */ $request = new $class($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); $contentType = $request->server('CONTENT_TYPE', ''); diff --git a/oauth/OAuth2/RequestInterface.php b/oauth/OAuth2/RequestInterface.php index 8a70d5f..1d036b7 100644 --- a/oauth/OAuth2/RequestInterface.php +++ b/oauth/OAuth2/RequestInterface.php @@ -4,13 +4,36 @@ namespace OAuth2; interface RequestInterface { + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function query($name, $default = null); + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function request($name, $default = null); + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function server($name, $default = null); + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function headers($name, $default = null); + /** + * @return mixed + */ public function getAllQueryParameters(); } diff --git a/oauth/OAuth2/Response.php b/oauth/OAuth2/Response.php index fc1e62a..ccd797a 100644 --- a/oauth/OAuth2/Response.php +++ b/oauth/OAuth2/Response.php @@ -2,6 +2,8 @@ namespace OAuth2; +use InvalidArgumentException; + /** * Class to handle OAuth2 Responses in a graceful way. Use this interface * to output the proper OAuth2 responses. @@ -13,12 +15,34 @@ namespace OAuth2; */ class Response implements ResponseInterface { + /** + * @var string + */ public $version; + + /** + * @var int + */ protected $statusCode = 200; + + /** + * @var string + */ protected $statusText; + + /** + * @var array + */ protected $parameters = array(); + + /** + * @var array + */ protected $httpHeaders = array(); + /** + * @var array + */ public static $statusTexts = array( 100 => 'Continue', 101 => 'Switching Protocols', @@ -63,6 +87,11 @@ class Response implements ResponseInterface 505 => 'HTTP Version Not Supported', ); + /** + * @param array $parameters + * @param int $statusCode + * @param array $headers + */ public function __construct($parameters = array(), $statusCode = 200, $headers = array()) { $this->setParameters($parameters); @@ -102,76 +131,128 @@ class Response implements ResponseInterface return sprintf("%s: %s\n", $name, $value); } + /** + * @return int + */ public function getStatusCode() { return $this->statusCode; } + /** + * @param int $statusCode + * @param string $text + * @throws InvalidArgumentException + */ public function setStatusCode($statusCode, $text = null) { $this->statusCode = (int) $statusCode; if ($this->isInvalid()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode)); + throw new InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode)); } $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text); } + /** + * @return string + */ public function getStatusText() { return $this->statusText; } + /** + * @return array + */ public function getParameters() { return $this->parameters; } + /** + * @param array $parameters + */ public function setParameters(array $parameters) { $this->parameters = $parameters; } + /** + * @param array $parameters + */ public function addParameters(array $parameters) { $this->parameters = array_merge($this->parameters, $parameters); } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function getParameter($name, $default = null) { return isset($this->parameters[$name]) ? $this->parameters[$name] : $default; } + /** + * @param string $name + * @param mixed $value + */ public function setParameter($name, $value) { $this->parameters[$name] = $value; } + /** + * @param array $httpHeaders + */ public function setHttpHeaders(array $httpHeaders) { $this->httpHeaders = $httpHeaders; } + /** + * @param string $name + * @param mixed $value + */ public function setHttpHeader($name, $value) { $this->httpHeaders[$name] = $value; } + /** + * @param array $httpHeaders + */ public function addHttpHeaders(array $httpHeaders) { $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders); } + /** + * @return array + */ public function getHttpHeaders() { return $this->httpHeaders; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function getHttpHeader($name, $default = null) { return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default; } + /** + * @param string $format + * @return mixed + * @throws InvalidArgumentException + */ public function getResponseBody($format = 'json') { switch ($format) { @@ -187,10 +268,13 @@ class Response implements ResponseInterface return $xml->asXML(); } - throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format)); + throw new InvalidArgumentException(sprintf('The format %s is not supported', $format)); } + /** + * @param string $format + */ public function send($format = 'json') { // headers have already been sent by the developer @@ -215,6 +299,14 @@ class Response implements ResponseInterface echo $this->getResponseBody($format); } + /** + * @param int $statusCode + * @param string $error + * @param string $errorDescription + * @param string $errorUri + * @return mixed + * @throws InvalidArgumentException + */ public function setError($statusCode, $error, $errorDescription = null, $errorUri = null) { $parameters = array( @@ -239,14 +331,24 @@ class Response implements ResponseInterface $this->addHttpHeaders($httpHeaders); if (!$this->isClientError() && !$this->isServerError()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); + throw new InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); } } + /** + * @param int $statusCode + * @param string $url + * @param string $state + * @param string $error + * @param string $errorDescription + * @param string $errorUri + * @return mixed + * @throws InvalidArgumentException + */ public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null) { if (empty($url)) { - throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + throw new InvalidArgumentException('Cannot redirect to an empty URL.'); } $parameters = array(); @@ -271,15 +373,16 @@ class Response implements ResponseInterface $this->addHttpHeaders(array('Location' => $url)); if (!$this->isRedirection()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); + throw new InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); } } - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html /** * @return Boolean * * @api + * + * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html */ public function isInvalid() { @@ -336,8 +439,11 @@ class Response implements ResponseInterface return $this->statusCode >= 500 && $this->statusCode < 600; } - /* - * Functions from Symfony2 HttpFoundation - output pretty header + /** + * Function from Symfony2 HttpFoundation - output pretty header + * + * @param array $headers + * @return string */ private function getHttpHeadersAsString($headers) { @@ -357,11 +463,23 @@ class Response implements ResponseInterface return $content; } + /** + * Function from Symfony2 HttpFoundation - output pretty header + * + * @param string $name + * @return mixed + */ private function beautifyHeaderName($name) { return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name)); } + /** + * Function from Symfony2 HttpFoundation - output pretty header + * + * @param array $match + * @return string + */ private function beautifyCallback($match) { return '-'.strtoupper($match[1]); diff --git a/oauth/OAuth2/ResponseInterface.php b/oauth/OAuth2/ResponseInterface.php index c99b5f7..fe92086 100644 --- a/oauth/OAuth2/ResponseInterface.php +++ b/oauth/OAuth2/ResponseInterface.php @@ -6,19 +6,48 @@ namespace OAuth2; * Interface which represents an object response. Meant to handle and display the proper OAuth2 Responses * for errors and successes * - * @see OAuth2\Response + * @see \OAuth2\Response */ interface ResponseInterface { + /** + * @param array $parameters + */ public function addParameters(array $parameters); + /** + * @param array $httpHeaders + */ public function addHttpHeaders(array $httpHeaders); + /** + * @param int $statusCode + */ public function setStatusCode($statusCode); + /** + * @param int $statusCode + * @param string $name + * @param string $description + * @param string $uri + * @return mixed + */ public function setError($statusCode, $name, $description = null, $uri = null); + /** + * @param int $statusCode + * @param string $url + * @param string $state + * @param string $error + * @param string $errorDescription + * @param string $errorUri + * @return mixed + */ public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null); + /** + * @param string $name + * @return mixed + */ public function getParameter($name); } diff --git a/oauth/OAuth2/ResponseType/AccessToken.php b/oauth/OAuth2/ResponseType/AccessToken.php index beffb11..e836a34 100644 --- a/oauth/OAuth2/ResponseType/AccessToken.php +++ b/oauth/OAuth2/ResponseType/AccessToken.php @@ -4,28 +4,39 @@ namespace OAuth2\ResponseType; use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface; use OAuth2\Storage\RefreshTokenInterface; +use RuntimeException; /** - * * @author Brent Shaffer */ class AccessToken implements AccessTokenInterface { + /** + * @var AccessTokenInterface + */ protected $tokenStorage; + + /** + * @var RefreshTokenInterface + */ protected $refreshStorage; + + /** + * @var array + */ protected $config; /** - * @param OAuth2\Storage\AccessTokenInterface $tokenStorage REQUIRED Storage class for saving access token information - * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage OPTIONAL Storage class for saving refresh token information - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'token_type' => 'bearer', // token type identifier - * 'access_lifetime' => 3600, // time before access token expires - * 'refresh_token_lifetime' => 1209600, // time before refresh token expires - * ); - * + * @param AccessTokenStorageInterface $tokenStorage - REQUIRED Storage class for saving access token information + * @param RefreshTokenInterface $refreshStorage - OPTIONAL Storage class for saving refresh token information + * @param array $config - OPTIONAL Configuration options for the server + * @code + * $config = array( + * 'token_type' => 'bearer', // token type identifier + * 'access_lifetime' => 3600, // time before access token expires + * 'refresh_token_lifetime' => 1209600, // time before refresh token expires + * ); + * @endcode */ public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array()) { @@ -39,6 +50,13 @@ class AccessToken implements AccessTokenInterface ), $config); } + /** + * Get authorize response + * + * @param array $params + * @param mixed $user_id + * @return array + */ public function getAuthorizeResponse($params, $user_id = null) { // build the URL to redirect to @@ -64,21 +82,22 @@ class AccessToken implements AccessTokenInterface /** * Handle the creation of access token, also issue refresh token if supported / desirable. * - * @param $client_id client identifier related to the access token. - * @param $user_id user ID associated with the access token - * @param $scope OPTIONAL scopes to be stored in space-separated string. - * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user ID associated with the access token + * @param string $scope - OPTIONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken - if true, a new refresh_token will be added to the response + * @return array * * @see http://tools.ietf.org/html/rfc6749#section-5 * @ingroup oauth2_section_5 */ public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) { - $includeRefreshToken = true; $token = array( "access_token" => $this->generateAccessToken(), - "token_type" => $this->config['token_type'] -// "expires_in" => $this->config['access_lifetime'], + "expires_in" => $this->config['access_lifetime'], + "token_type" => $this->config['token_type'], + "scope" => $scope ); $this->tokenStorage->setAccessToken($token["access_token"], $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); @@ -98,9 +117,6 @@ class AccessToken implements AccessTokenInterface $this->refreshStorage->setRefreshToken($token['refresh_token'], $client_id, $user_id, $expires, $scope); } - $token["scope"] = $scope; - $token["created_at"] = time(); - return $token; } @@ -110,13 +126,18 @@ class AccessToken implements AccessTokenInterface * Implementing classes may want to override this function to implement * other access token generation schemes. * - * @return - * An unique access token. + * @return string - A unique access token. * * @ingroup oauth2_section_4 */ protected function generateAccessToken() { + if (function_exists('random_bytes')) { + $randomData = random_bytes(20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } if (function_exists('openssl_random_pseudo_bytes')) { $randomData = openssl_random_pseudo_bytes(20); if ($randomData !== false && strlen($randomData) === 20) { @@ -147,8 +168,7 @@ class AccessToken implements AccessTokenInterface * Implementing classes may want to override this function to implement * other refresh token generation schemes. * - * @return - * An unique refresh. + * @return string - A unique refresh token. * * @ingroup oauth2_section_4 * @see OAuth2::generateAccessToken() @@ -165,6 +185,7 @@ class AccessToken implements AccessTokenInterface * * @param $token * @param null $tokenTypeHint + * @throws RuntimeException * @return boolean */ public function revokeToken($token, $tokenTypeHint = null) @@ -177,7 +198,7 @@ class AccessToken implements AccessTokenInterface /** @TODO remove in v2 */ if (!method_exists($this->tokenStorage, 'unsetAccessToken')) { - throw new \RuntimeException( + throw new RuntimeException( sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage) )); } diff --git a/oauth/OAuth2/ResponseType/AccessTokenInterface.php b/oauth/OAuth2/ResponseType/AccessTokenInterface.php index 4bd3928..0e576df 100644 --- a/oauth/OAuth2/ResponseType/AccessTokenInterface.php +++ b/oauth/OAuth2/ResponseType/AccessTokenInterface.php @@ -3,7 +3,6 @@ namespace OAuth2\ResponseType; /** - * * @author Brent Shaffer */ interface AccessTokenInterface extends ResponseTypeInterface @@ -11,10 +10,10 @@ interface AccessTokenInterface extends ResponseTypeInterface /** * Handle the creation of access token, also issue refresh token if supported / desirable. * - * @param $client_id client identifier related to the access token. - * @param $user_id user ID associated with the access token - * @param $scope OPTONAL scopes to be stored in space-separated string. - * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user ID associated with the access token + * @param string $scope - OPTONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken - if true, a new refresh_token will be added to the response * * @see http://tools.ietf.org/html/rfc6749#section-5 * @ingroup oauth2_section_5 @@ -31,4 +30,4 @@ interface AccessTokenInterface extends ResponseTypeInterface * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x */ //public function revokeToken($token, $tokenTypeHint); -} +} \ No newline at end of file diff --git a/oauth/OAuth2/ResponseType/AuthorizationCode.php b/oauth/OAuth2/ResponseType/AuthorizationCode.php index ecda4d7..8956ea2 100644 --- a/oauth/OAuth2/ResponseType/AuthorizationCode.php +++ b/oauth/OAuth2/ResponseType/AuthorizationCode.php @@ -5,7 +5,6 @@ namespace OAuth2\ResponseType; use OAuth2\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface; /** - * * @author Brent Shaffer */ class AuthorizationCode implements AuthorizationCodeInterface @@ -62,7 +61,7 @@ class AuthorizationCode implements AuthorizationCodeInterface { $this->storage->setUsersID($user_id); } - + // Scope API is forced here to comply with gitlab specification. $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], "api"); @@ -92,7 +91,9 @@ class AuthorizationCode implements AuthorizationCodeInterface protected function generateAuthorizationCode() { $tokenLen = 40; - if (function_exists('openssl_random_pseudo_bytes')) { + if (function_exists('random_bytes')) { + $randomData = random_bytes(100); + } elseif (function_exists('openssl_random_pseudo_bytes')) { $randomData = openssl_random_pseudo_bytes(100); } elseif (function_exists('mcrypt_create_iv')) { $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); diff --git a/oauth/OAuth2/ResponseType/AuthorizationCodeInterface.php b/oauth/OAuth2/ResponseType/AuthorizationCodeInterface.php index df777e2..4f0a29d 100644 --- a/oauth/OAuth2/ResponseType/AuthorizationCodeInterface.php +++ b/oauth/OAuth2/ResponseType/AuthorizationCodeInterface.php @@ -3,7 +3,6 @@ namespace OAuth2\ResponseType; /** - * * @author Brent Shaffer */ interface AuthorizationCodeInterface extends ResponseTypeInterface @@ -17,11 +16,12 @@ interface AuthorizationCodeInterface extends ResponseTypeInterface /** * Handle the creation of the authorization code. * - * @param $client_id client identifier related to the authorization code - * @param $user_id user id associated with the authorization code - * @param $redirect_uri an absolute URI to which the authorization server will redirect the - * user-agent to when the end-user authorization step is completed. - * @param $scope OPTIONAL scopes to be stored in space-separated string. + * @param mixed $client_id - Client identifier related to the authorization code + * @param mixed $user_id - User ID associated with the authorization code + * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. + * @return string * * @see http://tools.ietf.org/html/rfc6749#section-4 * @ingroup oauth2_section_4 diff --git a/oauth/OAuth2/ResponseType/JwtAccessToken.php b/oauth/OAuth2/ResponseType/JwtAccessToken.php new file mode 100644 index 0000000..0ee3708 --- /dev/null +++ b/oauth/OAuth2/ResponseType/JwtAccessToken.php @@ -0,0 +1,159 @@ + + */ +class JwtAccessToken extends AccessToken +{ + protected $publicKeyStorage; + protected $encryptionUtil; + + /** + * @param PublicKeyInterface $publicKeyStorage - + * @param AccessTokenStorageInterface $tokenStorage - + * @param RefreshTokenInterface $refreshStorage - + * @param array $config - array with key store_encrypted_token_string (bool true) + * whether the entire encrypted string is stored, + * or just the token ID is stored + * @param EncryptionInterface $encryptionUtil - + */ + public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $config = array_merge(array( + 'store_encrypted_token_string' => true, + 'issuer' => '' + ), $config); + if (is_null($tokenStorage)) { + // a pass-thru, so we can call the parent constructor + $tokenStorage = new Memory(); + } + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + parent::__construct($tokenStorage, $refreshStorage, $config); + } + + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param mixed $client_id - Client identifier related to the access token. + * @param mixed $user_id - User ID associated with the access token + * @param string $scope - (optional) Scopes to be stored in space-separated string. + * @param bool $includeRefreshToken - If true, a new refresh_token will be added to the response + * @return array - The access token + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) + { + // payload to encrypt + $payload = $this->createPayload($client_id, $user_id, $scope); + + /* + * Encode the payload data into a single JWT access_token string + */ + $access_token = $this->encodeToken($payload, $client_id); + + /* + * Save the token to a secondary storage. This is implemented on the + * OAuth2\Storage\JwtAccessToken side, and will not actually store anything, + * if no secondary storage has been supplied + */ + $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $payload['id']; + $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); + + // token to return to the client + $token = array( + 'access_token' => $access_token, + 'expires_in' => $this->config['access_lifetime'], + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + /* + * Issue a refresh token also, if we support them + * + * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface + * is supplied in the constructor + */ + if ($includeRefreshToken && $this->refreshStorage) { + $refresh_token = $this->generateRefreshToken(); + $expires = 0; + if ($this->config['refresh_token_lifetime'] > 0) { + $expires = time() + $this->config['refresh_token_lifetime']; + } + $this->refreshStorage->setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope); + $token['refresh_token'] = $refresh_token; + } + + return $token; + } + + /** + * @param array $token + * @param mixed $client_id + * @return mixed + */ + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } + + /** + * This function can be used to create custom JWT payloads + * + * @param mixed $client_id - Client identifier related to the access token. + * @param mixed $user_id - User ID associated with the access token + * @param string $scope - (optional) Scopes to be stored in space-separated string. + * @return array - The access token + */ + protected function createPayload($client_id, $user_id, $scope = null) + { + // token to encrypt + $expires = time() + $this->config['access_lifetime']; + $id = $this->generateAccessToken(); + + $payload = array( + 'id' => $id, // for BC (see #591) + 'jti' => $id, + 'iss' => $this->config['issuer'], + 'aud' => $client_id, + 'sub' => $user_id, + 'exp' => $expires, + 'iat' => time(), + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + if (isset($this->config['jwt_extra_payload_callable'])) { + if (!is_callable($this->config['jwt_extra_payload_callable'])) { + throw new \InvalidArgumentException('jwt_extra_payload_callable is not callable'); + } + + $extra = call_user_func($this->config['jwt_extra_payload_callable'], $client_id, $user_id, $scope); + + if (!is_array($extra)) { + throw new \InvalidArgumentException('jwt_extra_payload_callable must return array'); + } + + $payload = array_merge($extra, $payload); + } + + return $payload; + } +} diff --git a/oauth/OAuth2/ResponseType/ResponseTypeInterface.php b/oauth/OAuth2/ResponseType/ResponseTypeInterface.php index f8e26a5..a271565 100644 --- a/oauth/OAuth2/ResponseType/ResponseTypeInterface.php +++ b/oauth/OAuth2/ResponseType/ResponseTypeInterface.php @@ -4,5 +4,10 @@ namespace OAuth2\ResponseType; interface ResponseTypeInterface { + /** + * @param array $params + * @param mixed $user_id + * @return mixed + */ public function getAuthorizeResponse($params, $user_id = null); } diff --git a/oauth/OAuth2/Scope.php b/oauth/OAuth2/Scope.php index 9b6d6c4..3ba6e53 100644 --- a/oauth/OAuth2/Scope.php +++ b/oauth/OAuth2/Scope.php @@ -2,18 +2,23 @@ namespace OAuth2; +use InvalidArgumentException; +use OAuth2\Storage\Memory; use OAuth2\Storage\ScopeInterface as ScopeStorageInterface; /** -* @see OAuth2\ScopeInterface +* @see ScopeInterface */ class Scope implements ScopeInterface { protected $storage; /** - * @param mixed @storage - * Either an array of supported scopes, or an instance of OAuth2\Storage\ScopeInterface + * Constructor + * + * @param mixed $storage - Either an array of supported scopes, or an instance of OAuth2\Storage\ScopeInterface + * + * @throws InvalidArgumentException */ public function __construct($storage = null) { @@ -22,7 +27,7 @@ class Scope implements ScopeInterface } if (!$storage instanceof ScopeStorageInterface) { - throw new \InvalidArgumentException("Argument 1 to OAuth2\Scope must be null, an array, or instance of OAuth2\Storage\ScopeInterface"); + throw new InvalidArgumentException("Argument 1 to OAuth2\Scope must be null, an array, or instance of OAuth2\Storage\ScopeInterface"); } $this->storage = $storage; @@ -31,12 +36,10 @@ class Scope implements ScopeInterface /** * Check if everything in required scope is contained in available scope. * - * @param $required_scope - * A space-separated string of scopes. - * - * @return - * TRUE if everything in required scope is contained in available scope, - * and FALSE if it isn't. + * @param string $required_scope - A space-separated string of scopes. + * @param string $available_scope - A space-separated string of scopes. + * @return bool - TRUE if everything in required scope is contained in available scope and FALSE + * if it isn't. * * @see http://tools.ietf.org/html/rfc6749#section-7 * @@ -53,11 +56,8 @@ class Scope implements ScopeInterface /** * Check if the provided scope exists in storage. * - * @param $scope - * A space-separated string of scopes. - * - * @return - * TRUE if it exists, FALSE otherwise. + * @param string $scope - A space-separated string of scopes. + * @return bool - TRUE if it exists, FALSE otherwise. */ public function scopeExists($scope) { @@ -75,12 +75,20 @@ class Scope implements ScopeInterface } } + /** + * @param RequestInterface $request + * @return string + */ public function getScopeFromRequest(RequestInterface $request) { // "scope" is valid if passed in either POST or QUERY return $request->request('scope', $request->query('scope')); } + /** + * @param null $client_id + * @return mixed + */ public function getDefaultScope($client_id = null) { return $this->storage->getDefaultScope($client_id); @@ -92,8 +100,7 @@ class Scope implements ScopeInterface * In case OpenID Connect is used, these scopes must include: * 'openid', offline_access'. * - * @return - * An array of reserved scopes. + * @return array - An array of reserved scopes. */ public function getReservedScopes() { diff --git a/oauth/OAuth2/ScopeInterface.php b/oauth/OAuth2/ScopeInterface.php index 5b60f9a..f65cfa7 100644 --- a/oauth/OAuth2/ScopeInterface.php +++ b/oauth/OAuth2/ScopeInterface.php @@ -7,19 +7,17 @@ use OAuth2\Storage\ScopeInterface as ScopeStorageInterface; /** * Class to handle scope implementation logic * - * @see OAuth2\Storage\ScopeInterface + * @see \OAuth2\Storage\ScopeInterface */ interface ScopeInterface extends ScopeStorageInterface { /** * Check if everything in required scope is contained in available scope. * - * @param $required_scope - * A space-separated string of scopes. - * - * @return - * TRUE if everything in required scope is contained in available scope, - * and FALSE if it isn't. + * @param string $required_scope - A space-separated string of scopes. + * @param string $available_scope - A space-separated string of scopes. + * @return boolean - TRUE if everything in required scope is contained in available scope and FALSE + * if it isn't. * * @see http://tools.ietf.org/html/rfc6749#section-7 * @@ -30,11 +28,8 @@ interface ScopeInterface extends ScopeStorageInterface /** * Return scope info from request * - * @param OAuth2\RequestInterface - * Request object to check - * - * @return - * string representation of requested scope + * @param RequestInterface $request - Request object to check + * @return string - representation of requested scope */ public function getScopeFromRequest(RequestInterface $request); } diff --git a/oauth/OAuth2/Server.php b/oauth/OAuth2/Server.php index 33e4d00..3a93d7a 100644 --- a/oauth/OAuth2/Server.php +++ b/oauth/OAuth2/Server.php @@ -4,6 +4,12 @@ namespace OAuth2; use OAuth2\Controller\ResourceControllerInterface; use OAuth2\Controller\ResourceController; +use OAuth2\OpenID\Controller\UserInfoControllerInterface; +use OAuth2\OpenID\Controller\UserInfoController; +use OAuth2\OpenID\Controller\AuthorizeController as OpenIDAuthorizeController; +use OAuth2\OpenID\ResponseType\AuthorizationCode as OpenIDAuthorizationCodeResponseType; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; +use OAuth2\OpenID\GrantType\AuthorizationCode as OpenIDAuthorizationCodeGrantType; use OAuth2\Controller\AuthorizeControllerInterface; use OAuth2\Controller\AuthorizeController; use OAuth2\Controller\TokenControllerInterface; @@ -13,27 +19,39 @@ use OAuth2\ClientAssertionType\HttpBasic; use OAuth2\ResponseType\ResponseTypeInterface; use OAuth2\ResponseType\AuthorizationCode as AuthorizationCodeResponseType; use OAuth2\ResponseType\AccessToken; +use OAuth2\ResponseType\JwtAccessToken; +use OAuth2\OpenID\ResponseType\CodeIdToken; +use OAuth2\OpenID\ResponseType\IdToken; +use OAuth2\OpenID\ResponseType\IdTokenToken; use OAuth2\TokenType\TokenTypeInterface; use OAuth2\TokenType\Bearer; use OAuth2\GrantType\GrantTypeInterface; +use OAuth2\GrantType\UserCredentials; +use OAuth2\GrantType\ClientCredentials; use OAuth2\GrantType\RefreshToken; use OAuth2\GrantType\AuthorizationCode; +use OAuth2\Storage\ClientCredentialsInterface; +use OAuth2\Storage\ClientInterface; +use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage; +use OAuth2\Storage\JwtAccessTokenInterface; +use InvalidArgumentException; +use LogicException; /** * Server class for OAuth2 * This class serves as a convience class which wraps the other Controller classes * -* @see OAuth2\Controller\ResourceController -* @see OAuth2\Controller\AuthorizeController -* @see OAuth2\Controller\TokenController +* @see \OAuth2\Controller\ResourceController +* @see \OAuth2\Controller\AuthorizeController +* @see \OAuth2\Controller\TokenController */ class Server implements ResourceControllerInterface, AuthorizeControllerInterface, - TokenControllerInterface + TokenControllerInterface, + UserInfoControllerInterface { - // misc properties /** - * @var Response + * @var ResponseInterface */ protected $response; @@ -47,7 +65,6 @@ class Server implements ResourceControllerInterface, */ protected $storages; - // servers /** * @var AuthorizeControllerInterface */ @@ -68,17 +85,34 @@ class Server implements ResourceControllerInterface, */ protected $userInfoController; - // config classes - protected $grantTypes; - protected $responseTypes; + /** + * @var array + */ + protected $grantTypes = array(); + + /** + * @var array + */ + protected $responseTypes = array(); + + /** + * @var TokenTypeInterface + */ protected $tokenType; /** * @var ScopeInterface */ protected $scopeUtil; + + /** + * @var ClientAssertionTypeInterface + */ protected $clientAssertionType; + /** + * @var array + */ protected $storageMap = array( 'access_token' => 'OAuth2\Storage\AccessTokenInterface', 'authorization_code' => 'OAuth2\Storage\AuthorizationCodeInterface', @@ -86,25 +120,33 @@ class Server implements ResourceControllerInterface, 'client' => 'OAuth2\Storage\ClientInterface', 'refresh_token' => 'OAuth2\Storage\RefreshTokenInterface', 'user_credentials' => 'OAuth2\Storage\UserCredentialsInterface', + 'user_claims' => 'OAuth2\OpenID\Storage\UserClaimsInterface', 'public_key' => 'OAuth2\Storage\PublicKeyInterface', + 'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface', 'scope' => 'OAuth2\Storage\ScopeInterface', ); + /** + * @var array + */ protected $responseTypeMap = array( 'token' => 'OAuth2\ResponseType\AccessTokenInterface', 'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface', + 'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface', + 'id_token token' => 'OAuth2\OpenID\ResponseType\IdTokenTokenInterface', + 'code id_token' => 'OAuth2\OpenID\ResponseType\CodeIdTokenInterface', ); /** - * @param mixed $storage (array or OAuth2\Storage) - single object or array of objects implementing the - * required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum) - * @param array $config specify a different token lifetime, token header name, etc - * @param array $grantTypes An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens - * @param array $responseTypes Response types to use. array keys should be "code" and and "token" for - * Access Token and Authorization Code response types - * @param \OAuth2\TokenType\TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" - * @param \OAuth2\ScopeInterface $scopeUtil The scope utility class to use to validate scope - * @param \OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic + * @param mixed $storage (array or OAuth2\Storage) - single object or array of objects implementing the + * required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum) + * @param array $config specify a different token lifetime, token header name, etc + * @param array $grantTypes An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens + * @param array $responseTypes Response types to use. array keys should be "code" and "token" for + * Access Token and Authorization Code response types + * @param TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" + * @param ScopeInterface $scopeUtil The scope utility class to use to validate scope + * @param ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic * * @ingroup oauth2_section_7 */ @@ -118,7 +160,10 @@ class Server implements ResourceControllerInterface, // merge all config values. These get passed to our controller objects $this->config = array_merge(array( + 'use_jwt_access_tokens' => false, + 'jwt_extra_payload_callable' => null, 'store_encrypted_token_string' => true, + 'use_openid_connect' => false, 'id_lifetime' => 3600, 'access_lifetime' => 3600, 'www_realm' => 'Service', @@ -144,8 +189,15 @@ class Server implements ResourceControllerInterface, $this->tokenType = $tokenType; $this->scopeUtil = $scopeUtil; $this->clientAssertionType = $clientAssertionType; + + if ($this->config['use_openid_connect']) { + $this->validateOpenIdConnect(); + } } + /** + * @return AuthorizeControllerInterface + */ public function getAuthorizeController() { if (is_null($this->authorizeController)) { @@ -155,6 +207,9 @@ class Server implements ResourceControllerInterface, return $this->authorizeController; } + /** + * @return TokenController + */ public function getTokenController() { if (is_null($this->tokenController)) { @@ -164,6 +219,9 @@ class Server implements ResourceControllerInterface, return $this->tokenController; } + /** + * @return ResourceControllerInterface + */ public function getResourceController() { if (is_null($this->resourceController)) { @@ -173,6 +231,9 @@ class Server implements ResourceControllerInterface, return $this->resourceController; } + /** + * @return UserInfoControllerInterface + */ public function getUserInfoController() { if (is_null($this->userInfoController)) { @@ -183,8 +244,6 @@ class Server implements ResourceControllerInterface, } /** - * every getter deserves a setter - * * @param AuthorizeControllerInterface $authorizeController */ public function setAuthorizeController(AuthorizeControllerInterface $authorizeController) @@ -193,8 +252,6 @@ class Server implements ResourceControllerInterface, } /** - * every getter deserves a setter - * * @param TokenControllerInterface $tokenController */ public function setTokenController(TokenControllerInterface $tokenController) @@ -203,8 +260,6 @@ class Server implements ResourceControllerInterface, } /** - * every getter deserves a setter - * * @param ResourceControllerInterface $resourceController */ public function setResourceController(ResourceControllerInterface $resourceController) @@ -213,8 +268,6 @@ class Server implements ResourceControllerInterface, } /** - * every getter deserves a setter - * * @param UserInfoControllerInterface $userInfoController */ public function setUserInfoController(UserInfoControllerInterface $userInfoController) @@ -222,17 +275,34 @@ class Server implements ResourceControllerInterface, $this->userInfoController = $userInfoController; } + /** + * Return claims about the authenticated end-user. + * This would be called from the "/UserInfo" endpoint as defined in the spec. + * + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object containing error messages (failure) or user claims (success) + * @return ResponseInterface + * + * @throws \InvalidArgumentException + * @throws \LogicException + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo + */ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getUserInfoController()->handleUserInfoRequest($request, $this->response); + + return $this->response; + } + /** * Grant or deny a requested access token. * This would be called from the "/token" endpoint as defined in the spec. * Obviously, you can call your endpoint whatever you want. * - * @param $request - \OAuth2\RequestInterface - * Request object to grant access token - * - * @param $response - \OAuth2\ResponseInterface - * Response object containing error messages (failure) or access token (success) - * + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object containing error messages (failure) or access token (success) * @return ResponseInterface * * @throws \InvalidArgumentException @@ -252,6 +322,11 @@ class Server implements ResourceControllerInterface, return $this->response; } + /** + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object + * @return mixed + */ public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null) { $this->response = is_null($response) ? new Response() : $response; @@ -285,25 +360,18 @@ class Server implements ResourceControllerInterface, * authorization server should call this function to redirect the user * appropriately. * - * @param $request - * The request should have the follow parameters set in the querystring: - * - response_type: The requested response: an access token, an - * authorization code, or both. + * @param RequestInterface $request - The request should have the follow parameters set in the querystring: + * - response_type: The requested response: an access token, an authorization code, or both. * - client_id: The client identifier as described in Section 2. - * - redirect_uri: An absolute URI to which the authorization server - * will redirect the user-agent to when the end-user authorization - * step is completed. - * - scope: (optional) The scope of the resource request expressed as a - * list of space-delimited strings. - * - state: (optional) An opaque value used by the client to maintain - * state between the request and callback. - * @param ResponseInterface $response - * @param $is_authorized - * TRUE or FALSE depending on whether the user authorized the access. - * @param $user_id - * Identifier of user who authorized the client + * - redirect_uri: An absolute URI to which the authorization server will redirect the user-agent to when the + * end-user authorization step is completed. + * - scope: (optional) The scope of the resource request expressed as a list of space-delimited strings. + * - state: (optional) An opaque value used by the client to maintain state between the request and callback. * - * @return Response + * @param ResponseInterface $response - Response object + * @param bool $is_authorized - TRUE or FALSE depending on whether the user authorized the access. + * @param mixed $user_id - Identifier of user who authorized the client + * @return ResponseInterface * * @see http://tools.ietf.org/html/rfc6749#section-4 * @@ -320,14 +388,17 @@ class Server implements ResourceControllerInterface, /** * Pull the authorization request data out of the HTTP request. * - The redirect_uri is OPTIONAL as per draft 20. But your implementation can enforce it - * by setting $config['enforce_redirect'] to true. + * by setting $config['enforce_redirect'] to true. * - The state is OPTIONAL but recommended to enforce CSRF. Draft 21 states, however, that - * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true. + * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true. * * The draft specifies that the parameters should be retrieved from GET, override the Response * object to change this * - * @return + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @return bool + * * The authorization parameters so the authorization server can prompt * the user for approval if valid. * @@ -344,6 +415,12 @@ class Server implements ResourceControllerInterface, return $value; } + /** + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @param string $scope - Scope + * @return mixed + */ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null) { $this->response = is_null($response) ? new Response() : $response; @@ -352,6 +429,11 @@ class Server implements ResourceControllerInterface, return $value; } + /** + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @return mixed + */ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null) { $this->response = is_null($response) ? new Response() : $response; @@ -360,10 +442,14 @@ class Server implements ResourceControllerInterface, return $value; } + /** + * @param GrantTypeInterface $grantType + * @param mixed $identifier + */ public function addGrantType(GrantTypeInterface $grantType, $identifier = null) { if (!is_string($identifier)) { - $identifier = $grantType->getQuerystringIdentifier(); + $identifier = $grantType->getQueryStringIdentifier(); } $this->grantTypes[$identifier] = $grantType; @@ -377,11 +463,10 @@ class Server implements ResourceControllerInterface, /** * Set a storage object for the server * - * @param $storage - * An object implementing one of the Storage interfaces - * @param $key - * If null, the storage is set to the key of each storage interface it implements + * @param object $storage - An object implementing one of the Storage interfaces + * @param mixed $key - If null, the storage is set to the key of each storage interface it implements * + * @throws InvalidArgumentException * @see storageMap */ public function addStorage($storage, $key = null) @@ -395,11 +480,11 @@ class Server implements ResourceControllerInterface, // special logic to handle "client" and "client_credentials" strangeness if ($key === 'client' && !isset($this->storages['client_credentials'])) { - if ($storage instanceof \OAuth2\Storage\ClientCredentialsInterface) { + if ($storage instanceof ClientCredentialsInterface) { $this->storages['client_credentials'] = $storage; } } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) { - if ($storage instanceof \OAuth2\Storage\ClientInterface) { + if ($storage instanceof ClientInterface) { $this->storages['client'] = $storage; } } @@ -420,6 +505,12 @@ class Server implements ResourceControllerInterface, } } + /** + * @param ResponseTypeInterface $responseType + * @param mixed $key + * + * @throws InvalidArgumentException + */ public function addResponseType(ResponseTypeInterface $responseType, $key = null) { $key = $this->normalizeResponseType($key); @@ -446,6 +537,9 @@ class Server implements ResourceControllerInterface, } } + /** + * @return ScopeInterface + */ public function getScopeUtil() { if (!$this->scopeUtil) { @@ -457,8 +551,6 @@ class Server implements ResourceControllerInterface, } /** - * every getter deserves a setter - * * @param ScopeInterface $scopeUtil */ public function setScopeUtil($scopeUtil) @@ -466,6 +558,10 @@ class Server implements ResourceControllerInterface, $this->scopeUtil = $scopeUtil; } + /** + * @return AuthorizeControllerInterface + * @throws LogicException + */ protected function createDefaultAuthorizeController() { if (!isset($this->storages['client'])) { @@ -474,12 +570,26 @@ class Server implements ResourceControllerInterface, if (0 == count($this->responseTypes)) { $this->responseTypes = $this->getDefaultResponseTypes(); } + if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) { + $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $this->responseTypes['id_token token'] = $this->createDefaultIdTokenTokenResponseType(); + } + } $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri'))); + if ($this->config['use_openid_connect']) { + return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); } + /** + * @return TokenControllerInterface + * @throws LogicException + */ protected function createDefaultTokenController() { if (0 == count($this->grantTypes)) { @@ -501,7 +611,7 @@ class Server implements ResourceControllerInterface, } if (!isset($this->storages['client'])) { - throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server'); + throw new LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server"); } $accessTokenResponseType = $this->getAccessTokenResponseType(); @@ -509,10 +619,19 @@ class Server implements ResourceControllerInterface, return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil()); } + /** + * @return ResourceControllerInterface + * @throws LogicException + */ protected function createDefaultResourceController() { - if (!isset($this->storages['access_token'])) { - throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface'); + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the resource server'); } if (!$this->tokenType) { @@ -524,10 +643,23 @@ class Server implements ResourceControllerInterface, return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil()); } + /** + * @return UserInfoControllerInterface + * @throws LogicException + */ protected function createDefaultUserInfoController() { - if (!isset($this->storages['access_token'])) { - throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface'); + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server'); + } + + if (!isset($this->storages['user_claims'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server'); } if (!$this->tokenType) { @@ -539,6 +671,9 @@ class Server implements ResourceControllerInterface, return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil()); } + /** + * @return Bearer + */ protected function getDefaultTokenType() { $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name'))); @@ -546,6 +681,10 @@ class Server implements ResourceControllerInterface, return new Bearer($config); } + /** + * @return array + * @throws LogicException + */ protected function getDefaultResponseTypes() { $responseTypes = array(); @@ -554,9 +693,24 @@ class Server implements ResourceControllerInterface, $responseTypes['token'] = $this->getAccessTokenResponseType(); } + if ($this->config['use_openid_connect']) { + $responseTypes['id_token'] = $this->getIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $responseTypes['id_token token'] = $this->getIdTokenTokenResponseType(); + } + } + if (isset($this->storages['authorization_code'])) { $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); - $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config); + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); + } + $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config); + $responseTypes['code id_token'] = new CodeIdToken($responseTypes['code'], $responseTypes['id_token']); + } else { + $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config); + } } if (count($responseTypes) == 0) { @@ -566,6 +720,10 @@ class Server implements ResourceControllerInterface, return $responseTypes; } + /** + * @return array + * @throws LogicException + */ protected function getDefaultGrantTypes() { $grantTypes = array(); @@ -585,8 +743,14 @@ class Server implements ResourceControllerInterface, } if (isset($this->storages['authorization_code'])) { - $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']); - + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); + } + $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']); + } else { + $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']); + } } if (count($grantTypes) == 0) { @@ -596,15 +760,25 @@ class Server implements ResourceControllerInterface, return $grantTypes; } + /** + * @return AccessToken + */ protected function getAccessTokenResponseType() { if (isset($this->responseTypes['token'])) { return $this->responseTypes['token']; } + if ($this->config['use_jwt_access_tokens']) { + return $this->createDefaultJwtAccessTokenResponseType(); + } + return $this->createDefaultAccessTokenResponseType(); } + /** + * @return IdToken + */ protected function getIdTokenResponseType() { if (isset($this->responseTypes['id_token'])) { @@ -614,6 +788,9 @@ class Server implements ResourceControllerInterface, return $this->createDefaultIdTokenResponseType(); } + /** + * @return IdTokenToken + */ protected function getIdTokenTokenResponseType() { if (isset($this->responseTypes['id_token token'])) { @@ -623,10 +800,60 @@ class Server implements ResourceControllerInterface, return $this->createDefaultIdTokenTokenResponseType(); } + /** + * For Resource Controller + * + * @return JwtAccessTokenStorage + * @throws LogicException + */ + protected function createDefaultJwtAccessTokenStorage() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); + } + $tokenStorage = null; + if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + // wrap the access token storage as required. + return new JwtAccessTokenStorage($this->storages['public_key'], $tokenStorage); + } + + /** + * For Authorize and Token Controllers + * + * @return JwtAccessToken + * @throws LogicException + */ + protected function createDefaultJwtAccessTokenResponseType() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); + } + + $tokenStorage = null; + if (isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime jwt_extra_payload_callable'))); + + return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); + } + + /** + * @return AccessToken + * @throws LogicException + */ protected function createDefaultAccessTokenResponseType() { if (!isset($this->storages['access_token'])) { - throw new \LogicException('You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server'); + throw new LogicException("You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server"); } $refreshStorage = null; @@ -640,10 +867,17 @@ class Server implements ResourceControllerInterface, return new AccessToken($this->storages['access_token'], $refreshStorage, $config); } + /** + * @return IdToken + * @throws LogicException + */ protected function createDefaultIdTokenResponseType() { + if (!isset($this->storages['user_claims'])) { + throw new LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect"); + } if (!isset($this->storages['public_key'])) { - throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect'); + throw new LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect"); } $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime'))); @@ -651,11 +885,29 @@ class Server implements ResourceControllerInterface, return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config); } + /** + * @return IdTokenToken + */ protected function createDefaultIdTokenTokenResponseType() { return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); } + /** + * @throws InvalidArgumentException + */ + protected function validateOpenIdConnect() + { + $authCodeGrant = $this->getGrantType('authorization_code'); + if (!empty($authCodeGrant) && !$authCodeGrant instanceof OpenIDAuthorizationCodeGrantType) { + throw new \InvalidArgumentException('You have enabled OpenID Connect, but supplied a grant type that does not support it.'); + } + } + + /** + * @param string $name + * @return string + */ protected function normalizeResponseType($name) { // for multiple-valued response types - make them alphabetical @@ -668,36 +920,60 @@ class Server implements ResourceControllerInterface, return $name; } + /** + * @return mixed + */ public function getResponse() { return $this->response; } + /** + * @return array + */ public function getStorages() { return $this->storages; } + /** + * @param string $name + * @return object|null + */ public function getStorage($name) { return isset($this->storages[$name]) ? $this->storages[$name] : null; } + /** + * @return array + */ public function getGrantTypes() { return $this->grantTypes; } + /** + * @param string $name + * @return object|null + */ public function getGrantType($name) { return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null; } + /** + * @return array + */ public function getResponseTypes() { return $this->responseTypes; } + /** + * @param string $name + * @return object|null + */ public function getResponseType($name) { // for multiple-valued response types - make them alphabetical @@ -706,23 +982,70 @@ class Server implements ResourceControllerInterface, return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null; } + /** + * @return TokenTypeInterface + */ public function getTokenType() { return $this->tokenType; } + /** + * @return ClientAssertionTypeInterface + */ public function getClientAssertionType() { return $this->clientAssertionType; } + /** + * @param string $name + * @param mixed $value + */ public function setConfig($name, $value) { $this->config[$name] = $value; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function getConfig($name, $default = null) { return isset($this->config[$name]) ? $this->config[$name] : $default; } + + + + /** + * Check if user has already connected to oauth server previously and got an associated ID. + * + * @param string $username + * Username of an LDAP user (often uid) + * + * @return bool + * True if user has already an ID, false else. + */ + public function userExists($username) + { + if (isset($this->storages['authorization_code'])) { + // Sanitize and format username + $user = strtolower(strip_tags(htmlspecialchars($username))); + + // Use getUsersID() method to retrieve associated ID, if an ID is returned so user already exists. + if ($this->storages['authorization_code']->getUsersID($user)) { + return true; + } + else { + return false; + } + } + else { + // Storage error + throw new Exception('Fatal Error : Storage is undifined'); + } + } + } diff --git a/oauth/OAuth2/Storage/AccessTokenInterface.php b/oauth/OAuth2/Storage/AccessTokenInterface.php index 85781c6..f9e32e7 100644 --- a/oauth/OAuth2/Storage/AccessTokenInterface.php +++ b/oauth/OAuth2/Storage/AccessTokenInterface.php @@ -15,17 +15,18 @@ interface AccessTokenInterface * * We need to retrieve access token data as we create and verify tokens. * - * @param $oauth_token - * oauth_token to be check with. + * @param string $oauth_token - oauth_token to be check with. * - * @return - * An associative array as below, and return NULL if the supplied oauth_token - * is invalid: - * - expires: Stored expiration in unix timestamp. - * - client_id: (optional) Stored client identifier. - * - user_id: (optional) Stored user identifier. - * - scope: (optional) Stored scope values in space-separated string. - * - id_token: (optional) Stored id_token (if "use_openid_connect" is true). + * @return array|null - An associative array as below, and return NULL if the supplied oauth_token is invalid: + * @code + * array( + * 'expires' => $expires, // Stored expiration in unix timestamp. + * 'client_id' => $client_id, // (optional) Stored client identifier. + * 'user_id' => $user_id, // (optional) Stored user identifier. + * 'scope' => $scope, // (optional) Stored scope values in space-separated string. + * 'id_token' => $id_token // (optional) Stored id_token (if "use_openid_connect" is true). + * ); + * @endcode * * @ingroup oauth2_section_7 */ @@ -36,11 +37,11 @@ interface AccessTokenInterface * * We need to store access token data as we create and verify tokens. * - * @param $oauth_token oauth_token to be stored. - * @param $client_id client identifier to be stored. - * @param $user_id user identifier to be stored. - * @param int $expires expiration to be stored as a Unix timestamp. - * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * @param string $oauth_token - oauth_token to be stored. + * @param mixed $client_id - client identifier to be stored. + * @param mixed $user_id - user identifier to be stored. + * @param int $expires - expiration to be stored as a Unix timestamp. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. * * @ingroup oauth2_section_4 */ @@ -60,8 +61,22 @@ interface AccessTokenInterface * * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x */ - //public function unsetAccessToken($access_token) + //public function unsetAccessToken($access_token); - public function getUsersID($username); +/*-------------------------------------------------------------------------------------------------------------------------------------------------*/ +/** +* @author Denis CLAVIER +*/ + /** + * Get user id on Oauth2 server + * + * @param string $username + * Username of an LDAP user (often uid) + * + * @return int|bool + * The id associated to username in users table + * and FALSE if username is not in the users table + */ + public function getUsersID($username); } diff --git a/oauth/OAuth2/Storage/AuthorizationCodeInterface.php b/oauth/OAuth2/Storage/AuthorizationCodeInterface.php index a5ae058..79bb45c 100644 --- a/oauth/OAuth2/Storage/AuthorizationCodeInterface.php +++ b/oauth/OAuth2/Storage/AuthorizationCodeInterface.php @@ -59,12 +59,12 @@ interface AuthorizationCodeInterface * * Required for OAuth2::GRANT_TYPE_AUTH_CODE. * - * @param string $code Authorization code to be stored. - * @param mixed $client_id Client identifier to be stored. - * @param mixed $user_id User identifier to be stored. - * @param string $redirect_uri Redirect URI(s) to be stored in a space-separated string. - * @param int $expires Expiration to be stored as a Unix timestamp. - * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * @param string $code - Authorization code to be stored. + * @param mixed $client_id - Client identifier to be stored. + * @param mixed $user_id - User identifier to be stored. + * @param string $redirect_uri - Redirect URI(s) to be stored in a space-separated string. + * @param int $expires - Expiration to be stored as a Unix timestamp. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. * * @ingroup oauth2_section_4 */ @@ -86,33 +86,32 @@ interface AuthorizationCodeInterface /*-------------------------------------------------------------------------------------------------------------------------------------------------*/ /** - * @author Denis CLAVIER - */ +* @author Denis CLAVIER +*/ - /** - * get user id on Oauth2 server - * - * @param string $username - * Username of an LDAP user (often uid) - * - * @return - * The id associated to username in users table - * and FALSE if username is not in the users table - */ - public function getUsersID($username); - - /** - * set an id for username on Oauth2 server - * - * @param string $username - * Username of an LDAP user (often uid) - * - * @return - * TRUE if insertion has succeed - * and FALSE if is not - * - * An unique ID is linked to the username after this function - */ - public function setUsersID($username); + /** + * Get user id on Oauth2 server + * + * @param string $username + * Username of an LDAP user (often uid) + * + * @return int|bool + * The id associated to username in users table + * and FALSE if username is not in the users table + */ + public function getUsersID($username); + /** + * Set an id for username on Oauth2 server + * + * @param string $username + * Username of an LDAP user (often uid) + * + * @return bool + * TRUE if insertion has succeed + * and FALSE if is not + * + * An unique ID is linked to the username after this function + */ + public function setUsersID($username); } diff --git a/oauth/OAuth2/Storage/Cassandra.php b/oauth/OAuth2/Storage/Cassandra.php new file mode 100644 index 0000000..e60e9d3 --- /dev/null +++ b/oauth/OAuth2/Storage/Cassandra.php @@ -0,0 +1,660 @@ + + * composer require thobbs/phpcassa:dev-master + * + * + * Once this is done, instantiate the connection: + * + * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); + * + * + * Then, register the storage client: + * + * $storage = new OAuth2\Storage\Cassandra($cassandra); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * + * + * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage + */ +class Cassandra implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /** + * @var ConnectionPool + */ + protected $cassandra; + + /** + * @var array + */ + protected $config; + + /** + * Cassandra Storage! uses phpCassa + * + * @param ConnectionPool|array $connection + * @param array $config + * + * @throws InvalidArgumentException + */ + public function __construct($connection = array(), array $config = array()) + { + if ($connection instanceof ConnectionPool) { + $this->cassandra = $connection; + } else { + if (!is_array($connection)) { + throw new InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array'); + } + $connection = array_merge(array( + 'keyspace' => 'oauth2', + 'servers' => null, + ), $connection); + + $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']); + } + + $this->config = array_merge(array( + // cassandra config + 'column_family' => 'auth', + + // key names + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + 'public_key_key' => 'oauth_public_keys:', + ), $config); + } + + /** + * @param $key + * @return bool|mixed + */ + protected function getValue($key) + { + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + try { + $value = $cf->get($key, new ColumnSlice("", "")); + $value = array_shift($value); + } catch (\cassandra\NotFoundException $e) { + return false; + } + + return json_decode($value, true); + } + + /** + * @param $key + * @param $value + * @param int $expire + * @return bool + */ + protected function setValue($key, $value, $expire = 0) + { + $this->cache[$key] = $value; + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + $str = json_encode($value); + if ($expire > 0) { + try { + $seconds = $expire - time(); + // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds + $cf->insert($key, array('__data' => $str), null, $seconds); + } catch (\Exception $e) { + return false; + } + } else { + try { + // __data key set as C* requires a field + $cf->insert($key, array('__data' => $str)); + } catch (\Exception $e) { + return false; + } + } + + return true; + } + + /** + * @param $key + * @return bool + */ + protected function expireValue($key) + { + unset($this->cache[$key]); + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + if ($cf->get_count($key) > 0) { + try { + // __data key set as C* requires a field + $cf->remove($key, array('__data')); + } catch (\Exception $e) { + return false; + } + + return true; + } + + return false; + } + + /** + * @param string $code + * @return bool|mixed + */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + /** + * @param string $authorization_code + * @param mixed $client_id + * @param mixed $user_id + * @param string $redirect_uri + * @param int $expires + * @param string $scope + * @param string $id_token + * @return bool + */ + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + /** + * @param string $code + * @return bool + */ + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /** + * @param string $username + * @param string $password + * @return bool + */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + /** + * plaintext passwords are bad! Override this for your application + * + * @param array $user + * @param string $password + * @return bool + */ + protected function checkPassword($user, $password) + { + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); + } + + /** + * @param string $username + * @return array|bool|false + */ + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /** + * @param string $username + * @return array|bool + */ + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + /** + * @param string $username + * @param string $password + * @param string $first_name + * @param string $last_name + * @return bool + */ + public function setUser($username, $password, $first_name = null, $last_name = null) + { + $password = $this->hashPassword($password); + + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /** + * @param mixed $client_id + * @param string $client_secret + * @return bool + */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + /** + * @param $client_id + * @return bool + */ + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($client['client_secret']); + } + + /** + * @param $client_id + * @return array|bool|mixed + */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + /** + * @param $client_id + * @param null $client_secret + * @param null $redirect_uri + * @param null $grant_types + * @param null $scope + * @param null $user_id + * @return bool + */ + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + /** + * @param $client_id + * @param $grant_type + * @return bool + */ + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /** + * @param $refresh_token + * @return bool|mixed + */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + /** + * @param $refresh_token + * @param $client_id + * @param $user_id + * @param $expires + * @param null $scope + * @return bool + */ + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + /** + * @param $refresh_token + * @return bool + */ + public function unsetRefreshToken($refresh_token) + { + return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + } + + /** + * @param string $access_token + * @return array|bool|mixed|null + */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + /** + * @param string $access_token + * @param mixed $client_id + * @param mixed $user_id + * @param int $expires + * @param null $scope + * @return bool + */ + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + /** + * @param $access_token + * @return bool + */ + public function unsetAccessToken($access_token) + { + return $this->expireValue($this->config['access_token_key'] . $access_token); + } + + /** + * @param $scope + * @return bool + */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + /** + * @param null $client_id + * @return bool|mixed + */ + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + /** + * @param $scope + * @param null $client_id + * @param string $type + * @return bool + * @throws \InvalidArgumentException + */ + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /** + * @param $client_id + * @param $subject + * @return bool|null + */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject ) { + return $jwt['key']; + } + + return null; + } + + /** + * @param $client_id + * @param $key + * @param null $subject + * @return bool + */ + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + /** + * @param $client_id + * @return bool|null + */ + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + /** + * @param $client_id + * @param $subject + * @param $audience + * @param $expiration + * @param $jti + * @throws \Exception + */ + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.'); + } + + /** + * @param $client_id + * @param $subject + * @param $audience + * @param $expiration + * @param $jti + * @throws \Exception + */ + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); + } + + /** + * @param string $client_id + * @return mixed + */ + public function getPublicKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['public_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['public_key']; + } + } + + /** + * @param string $client_id + * @return mixed + */ + public function getPrivateKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['private_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['private_key']; + } + } + + /** + * @param null $client_id + * @return mixed|string + */ + public function getEncryptionAlgorithm($client_id = null) + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + + return 'RS256'; + } + + /** + * @param mixed $user_id + * @param string $claims + * @return array|bool + */ + public function getUserClaims($user_id, $claims) + { + $userDetails = $this->getUserDetails($user_id); + if (!is_array($userDetails)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + /** + * @param $claim + * @param $userDetails + * @return array + */ + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + if ($value == 'email_verified') { + $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + } else { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + } + + return $userClaims; + } +} \ No newline at end of file diff --git a/oauth/OAuth2/Storage/CouchbaseDB.php b/oauth/OAuth2/Storage/CouchbaseDB.php index 1eb55f0..9e8148b 100755 --- a/oauth/OAuth2/Storage/CouchbaseDB.php +++ b/oauth/OAuth2/Storage/CouchbaseDB.php @@ -328,4 +328,4 @@ class CouchbaseDB implements AuthorizationCodeInterface, //TODO: Needs couchbase implementation. throw new \Exception('setJti() for the Couchbase driver is currently unimplemented.'); } -} +} \ No newline at end of file diff --git a/oauth/OAuth2/Storage/DynamoDB.php b/oauth/OAuth2/Storage/DynamoDB.php new file mode 100644 index 0000000..a54cb37 --- /dev/null +++ b/oauth/OAuth2/Storage/DynamoDB.php @@ -0,0 +1,540 @@ + + * composer require aws/aws-sdk-php:dev-master + * + * + * Once this is done, instantiate the DynamoDB client + * + * $storage = new OAuth2\Storage\Dynamodb(array("key" => "YOURKEY", "secret" => "YOURSECRET", "region" => "YOURREGION")); + * + * + * Table : + * - oauth_access_tokens (primary hash key : access_token) + * - oauth_authorization_codes (primary hash key : authorization_code) + * - oauth_clients (primary hash key : client_id) + * - oauth_jwt (primary hash key : client_id, primary range key : subject) + * - oauth_public_keys (primary hash key : client_id) + * - oauth_refresh_tokens (primary hash key : refresh_token) + * - oauth_scopes (primary hash key : scope, secondary index : is_default-index hash key is_default) + * - oauth_users (primary hash key : username) + * + * @author Frederic AUGUSTE + */ +class DynamoDB implements + AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + protected $client; + protected $config; + + public function __construct($connection, $config = array()) + { + if (!($connection instanceof DynamoDbClient)) { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + } + if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + } + $this->client = DynamoDbClient::factory(array( + 'key' => $connection["key"], + 'secret' => $connection["secret"], + 'region' =>$connection["region"] + )); + } else { + $this->client = $connection; + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', + ), $config); + } + + /* OAuth2\Storage\ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + + return $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret; + } + + public function isPublicClient($client_id) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + + if ($result->count()==0) { + return false ; + } + + return empty($result["Item"]["client_secret"]); + } + + /* OAuth2\Storage\ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $result = $this->dynamo2array($result); + foreach (array('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') as $key => $val) { + if (!array_key_exists ($val, $result)) { + $result[$val] = null; + } + } + + return $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['client_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* OAuth2\Storage\AccessTokenInterface */ + public function getAccessToken($access_token) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['access_token_table'], + "Key" => array('access_token' => array('S' => $access_token)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + if (array_key_exists ('expires', $token)) { + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['access_token_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + + } + + public function unsetAccessToken($access_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['access_token_table'], + 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)), + 'ReturnValues' => 'ALL_OLD', + )); + + return null !== $result->get('Attributes'); + } + + /* OAuth2\Storage\AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['code_table'], + "Key" => array('authorization_code' => array('S' => $code)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + if (!array_key_exists("id_token", $token )) { + $token['id_token'] = null; + } + $token['expires'] = strtotime($token['expires']); + + return $token; + + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['code_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function expireAuthorizationCode($code) + { + + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['code_table'], + 'Key' => $this->client->formatAttributes(array("authorization_code" => $code)) + )); + + return true; + } + + /* OAuth2\Storage\UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + if ($value == 'email_verified') { + $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + } else { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + } + + return $userClaims; + } + + /* OAuth2\Storage\RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['refresh_token_table'], + "Key" => array('refresh_token' => array('S' => $refresh_token)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + $token['expires'] = strtotime($token['expires']); + + return $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['refresh_token_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['refresh_token_table'], + 'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token)) + )); + + return true; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); + } + + public function getUser($username) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['user_table'], + "Key" => array('username' => array('S' => $username)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + $token['user_id'] = $username; + + return $token; + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + // do not store in plaintext + $password = $this->hashPassword($password); + + $clientData = compact('username', 'password', 'first_name', 'last_name'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['user_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + $scope_query = array(); + $count = 0; + foreach ($scope as $key => $val) { + $result = $this->client->query(array( + 'TableName' => $this->config['scope_table'], + 'Select' => 'COUNT', + 'KeyConditions' => array( + 'scope' => array( + 'AttributeValueList' => array(array('S' => $val)), + 'ComparisonOperator' => 'EQ' + ) + ) + )); + $count += $result['Count']; + } + + return $count == count($scope); + } + + public function getDefaultScope($client_id = null) + { + + $result = $this->client->query(array( + 'TableName' => $this->config['scope_table'], + 'IndexName' => 'is_default-index', + 'Select' => 'ALL_ATTRIBUTES', + 'KeyConditions' => array( + 'is_default' => array( + 'AttributeValueList' => array(array('S' => 'true')), + 'ComparisonOperator' => 'EQ', + ), + ) + )); + $defaultScope = array(); + if ($result->count() > 0) { + $array = $result->toArray(); + foreach ($array["Items"] as $item) { + $defaultScope[] = $item['scope']['S']; + } + + return empty($defaultScope) ? null : implode(' ', $defaultScope); + } + + return null; + } + + /* JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['jwt_table'], + "Key" => array('client_id' => array('S' => $client_id), 'subject' => array('S' => $subject)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['public_key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO not use. + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO not use. + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = '0') + { + + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['public_key']; + + } + + public function getPrivateKey($client_id = '0') + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return 'RS256' ; + } + $token = $this->dynamo2array($result); + + return $token['encryption_algorithm']; + } + + /** + * Transform dynamodb resultset to an array. + * @param $dynamodbResult + * @return $array + */ + private function dynamo2array($dynamodbResult) + { + $result = array(); + foreach ($dynamodbResult["Item"] as $key => $val) { + $result[$key] = $val["S"]; + $result[] = $val["S"]; + } + + return $result; + } + + private static function isNotEmpty($value) + { + return null !== $value && '' !== $value; + } +} \ No newline at end of file diff --git a/oauth/OAuth2/Storage/JwtAccessToken.php b/oauth/OAuth2/Storage/JwtAccessToken.php new file mode 100644 index 0000000..6ccacd6 --- /dev/null +++ b/oauth/OAuth2/Storage/JwtAccessToken.php @@ -0,0 +1,87 @@ + + */ +class JwtAccessToken implements JwtAccessTokenInterface +{ + protected $publicKeyStorage; + protected $tokenStorage; + protected $encryptionUtil; + + /** + * @param OAuth2\Encryption\PublicKeyInterface $publicKeyStorage the public key encryption to use + * @param OAuth2\Storage\AccessTokenInterface $tokenStorage OPTIONAL persist the access token to another storage. This is useful if + * you want to retain access token grant information somewhere, but + * is not necessary when using this grant type. + * @param OAuth2\Encryption\EncryptionInterface $encryptionUtil OPTIONAL class to use for "encode" and "decode" functions. + */ + public function __construct(PublicKeyInterface $publicKeyStorage, AccessTokenInterface $tokenStorage = null, EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $this->tokenStorage = $tokenStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt; + } + $this->encryptionUtil = $encryptionUtil; + } + + public function getAccessToken($oauth_token) + { + // just decode the token, don't verify + if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null, false)) { + return false; + } + + $client_id = isset($tokenData['aud']) ? $tokenData['aud'] : null; + $public_key = $this->publicKeyStorage->getPublicKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + // now that we have the client_id, verify the token + if (false === $this->encryptionUtil->decode($oauth_token, $public_key, array($algorithm))) { + return false; + } + + // normalize the JWT claims to the format expected by other components in this library + return $this->convertJwtToOAuth2($tokenData); + } + + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) + { + if ($this->tokenStorage) { + return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope); + } + } + + public function unsetAccessToken($access_token) + { + if ($this->tokenStorage) { + return $this->tokenStorage->unsetAccessToken($access_token); + } + } + + + // converts a JWT access token into an OAuth2-friendly format + protected function convertJwtToOAuth2($tokenData) + { + $keyMapping = array( + 'aud' => 'client_id', + 'exp' => 'expires', + 'sub' => 'user_id' + ); + + foreach ($keyMapping as $jwtKey => $oauth2Key) { + if (isset($tokenData[$jwtKey])) { + $tokenData[$oauth2Key] = $tokenData[$jwtKey]; + unset($tokenData[$jwtKey]); + } + } + + return $tokenData; + } +} \ No newline at end of file diff --git a/oauth/OAuth2/Storage/JwtAccessTokenInterface.php b/oauth/OAuth2/Storage/JwtAccessTokenInterface.php new file mode 100644 index 0000000..3abb2aa --- /dev/null +++ b/oauth/OAuth2/Storage/JwtAccessTokenInterface.php @@ -0,0 +1,14 @@ + + */ +interface JwtAccessTokenInterface extends AccessTokenInterface +{ + +} diff --git a/oauth/OAuth2/Storage/JwtBearerInterface.php b/oauth/OAuth2/Storage/JwtBearerInterface.php new file mode 100644 index 0000000..c83aa72 --- /dev/null +++ b/oauth/OAuth2/Storage/JwtBearerInterface.php @@ -0,0 +1,74 @@ + + */ +interface JwtBearerInterface +{ + /** + * Get the public key associated with a client_id + * + * @param $client_id + * Client identifier to be checked with. + * + * @return + * STRING Return the public key for the client_id if it exists, and MUST return FALSE if it doesn't. + */ + public function getClientKey($client_id, $subject); + + /** + * Get a jti (JSON token identifier) by matching against the client_id, subject, audience and expiration. + * + * @param $client_id + * Client identifier to match. + * + * @param $subject + * The subject to match. + * + * @param $audience + * The audience to match. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to match. + * + * @return + * An associative array as below, and return NULL if the jti does not exist. + * - issuer: Stored client identifier. + * - subject: Stored subject. + * - audience: Stored audience. + * - expires: Stored expiration in unix timestamp. + * - jti: The stored jti. + */ + public function getJti($client_id, $subject, $audience, $expiration, $jti); + + /** + * Store a used jti so that we can check against it to prevent replay attacks. + * @param $client_id + * Client identifier to insert. + * + * @param $subject + * The subject to insert. + * + * @param $audience + * The audience to insert. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to insert. + */ + public function setJti($client_id, $subject, $audience, $expiration, $jti); +} diff --git a/oauth/OAuth2/Storage/Memory.php b/oauth/OAuth2/Storage/Memory.php new file mode 100644 index 0000000..2c60b71 --- /dev/null +++ b/oauth/OAuth2/Storage/Memory.php @@ -0,0 +1,381 @@ + + */ +class Memory implements AuthorizationCodeInterface, + UserCredentialsInterface, + UserClaimsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + public $authorizationCodes; + public $userCredentials; + public $clientCredentials; + public $refreshTokens; + public $accessTokens; + public $jwt; + public $jti; + public $supportedScopes; + public $defaultScope; + public $keys; + + public function __construct($params = array()) + { + $params = array_merge(array( + 'authorization_codes' => array(), + 'user_credentials' => array(), + 'client_credentials' => array(), + 'refresh_tokens' => array(), + 'access_tokens' => array(), + 'jwt' => array(), + 'jti' => array(), + 'default_scope' => null, + 'supported_scopes' => array(), + 'keys' => array(), + ), $params); + + $this->authorizationCodes = $params['authorization_codes']; + $this->userCredentials = $params['user_credentials']; + $this->clientCredentials = $params['client_credentials']; + $this->refreshTokens = $params['refresh_tokens']; + $this->accessTokens = $params['access_tokens']; + $this->jwt = $params['jwt']; + $this->jti = $params['jti']; + $this->supportedScopes = $params['supported_scopes']; + $this->defaultScope = $params['default_scope']; + $this->keys = $params['keys']; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + if (!isset($this->authorizationCodes[$code])) { + return false; + } + + return array_merge(array( + 'authorization_code' => $code, + ), $this->authorizationCodes[$code]); + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'); + + return true; + } + + public function setAuthorizationCodes($authorization_codes) + { + $this->authorizationCodes = $authorization_codes; + } + + public function expireAuthorizationCode($code) + { + unset($this->authorizationCodes[$code]); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $userDetails = $this->getUserDetails($username); + + return $userDetails && $userDetails['password'] && $userDetails['password'] === $password; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + $this->userCredentials[$username] = array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName, + ); + + return true; + } + + public function getUserDetails($username) + { + if (!isset($this->userCredentials[$username])) { + return false; + } + + return array_merge(array( + 'user_id' => $username, + 'password' => null, + 'first_name' => null, + 'last_name' => null, + ), $this->userCredentials[$username]); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret; + } + + public function isPublicClient($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + return empty($this->clientCredentials[$client_id]['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + $clientDetails = array_merge(array( + 'client_id' => $client_id, + 'client_secret' => null, + 'redirect_uri' => null, + 'scope' => null, + ), $this->clientCredentials[$client_id]); + + return $clientDetails; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + if (isset($this->clientCredentials[$client_id]['grant_types'])) { + $grant_types = explode(' ', $this->clientCredentials[$client_id]['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $this->clientCredentials[$client_id] = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return isset($this->refreshTokens[$refresh_token]) ? $this->refreshTokens[$refresh_token] : false; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->refreshTokens[$refresh_token] = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + if (isset($this->refreshTokens[$refresh_token])) { + unset($this->refreshTokens[$refresh_token]); + + return true; + } + + return false; + } + + public function setRefreshTokens($refresh_tokens) + { + $this->refreshTokens = $refresh_tokens; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return isset($this->accessTokens[$access_token]) ? $this->accessTokens[$access_token] : false; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null, $id_token = null) + { + $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope', 'id_token'); + + return true; + } + + public function unsetAccessToken($access_token) + { + if (isset($this->accessTokens[$access_token])) { + unset($this->accessTokens[$access_token]); + + return true; + } + + return false; + } + + public function scopeExists($scope) + { + $scope = explode(' ', trim($scope)); + + return (count(array_diff($scope, $this->supportedScopes)) == 0); + } + + public function getDefaultScope($client_id = null) + { + return $this->defaultScope; + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (isset($this->jwt[$client_id])) { + $jwt = $this->jwt[$client_id]; + if ($jwt) { + if ($jwt["subject"] == $subject) { + return $jwt["key"]; + } + } + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + foreach ($this->jti as $storedJti) { + if ($storedJti['issuer'] == $client_id && $storedJti['subject'] == $subject && $storedJti['audience'] == $audience && $storedJti['expires'] == $expires && $storedJti['jti'] == $jti) { + return array( + 'issuer' => $storedJti['issuer'], + 'subject' => $storedJti['subject'], + 'audience' => $storedJti['audience'], + 'expires' => $storedJti['expires'], + 'jti' => $storedJti['jti'] + ); + } + } + + return null; + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $this->jti[] = array('issuer' => $client_id, 'subject' => $subject, 'audience' => $audience, 'expires' => $expires, 'jti' => $jti); + } + + /*PublicKeyInterface */ + public function getPublicKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['public_key']; + } + + // use a global encryption pair + if (isset($this->keys['public_key'])) { + return $this->keys['public_key']; + } + + return false; + } + + public function getPrivateKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['private_key']; + } + + // use a global encryption pair + if (isset($this->keys['private_key'])) { + return $this->keys['private_key']; + } + + return false; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if (isset($this->keys[$client_id]['encryption_algorithm'])) { + return $this->keys[$client_id]['encryption_algorithm']; + } + + // use a global encryption algorithm + if (isset($this->keys['encryption_algorithm'])) { + return $this->keys['encryption_algorithm']; + } + + return 'RS256'; + } +} \ No newline at end of file diff --git a/oauth/OAuth2/Storage/Mongo.php b/oauth/OAuth2/Storage/Mongo.php new file mode 100644 index 0000000..eea06e3 --- /dev/null +++ b/oauth/OAuth2/Storage/Mongo.php @@ -0,0 +1,392 @@ + + */ +class Mongo implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \MongoDB) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new \MongoClient($server); + $this->db = $m->{$connection['database']}; + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'key_table' => 'oauth_keys', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $this->collection('client_table')->update( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + } else { + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $this->collection('client_table')->insert($client); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->collection('access_token_table')->update( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + } else { + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $this->collection('access_token_table')->insert($token); + } + + return true; + } + + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->remove(array( + 'access_token' => $access_token + ), array('w' => 1)); + + return $result['n'] > 0; + } + + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array('authorization_code' => $code)); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->collection('code_table')->update( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + } else { + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $this->collection('code_table')->insert($token); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->collection('code_table')->remove(array('authorization_code' => $code)); + + return true; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array('refresh_token' => $refresh_token)); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $this->collection('refresh_token_table')->insert($token); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->collection('refresh_token_table')->remove(array( + 'refresh_token' => $refresh_token + ), array('w' => 1)); + + return $result['n'] > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->collection('user_table')->update( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + } else { + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $this->collection('user_table')->insert($user); + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } +} diff --git a/oauth/OAuth2/Storage/MongoDB.php b/oauth/OAuth2/Storage/MongoDB.php new file mode 100644 index 0000000..64f740f --- /dev/null +++ b/oauth/OAuth2/Storage/MongoDB.php @@ -0,0 +1,380 @@ + + */ +class MongoDB implements AuthorizationCodeInterface, + UserCredentialsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof Database) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB\Database or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new Client($server); + $this->db = $m->selectDatabase($connection['database']); + } + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'key_table' => 'oauth_keys', + ), $config); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $result = $this->collection('client_table')->updateOne( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + return $result->getMatchedCount() > 0; + } + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $result = $this->collection('client_table')->insertOne($client); + return $result->getInsertedCount() > 0; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + return in_array($grant_type, $grant_types); + } + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $result = $this->collection('access_token_table')->updateOne( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $result = $this->collection('access_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->deleteOne(array( + 'access_token' => $access_token + )); + return $result->getDeletedCount() > 0; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array( + 'authorization_code' => $code + )); + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $result = $this->collection('code_table')->updateOne( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $result = $this->collection('code_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function expireAuthorizationCode($code) + { + $result = $this->collection('code_table')->deleteOne(array( + 'authorization_code' => $code + )); + return $result->getDeletedCount() > 0; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array( + 'refresh_token' => $refresh_token + )); + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $result = $this->collection('refresh_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->collection('refresh_token_table')->deleteOne(array( + 'refresh_token' => $refresh_token + )); + return $result->getDeletedCount() > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $result = $this->collection('user_table')->updateOne( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + + return $result->getMatchedCount() > 0; + } + + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $result = $this->collection('user_table')->insertOne($user); + return $result->getInsertedCount() > 0; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } +} \ No newline at end of file diff --git a/oauth/OAuth2/Storage/Pdo.php b/oauth/OAuth2/Storage/Pdo.php index ce2caf7..1e23d3b 100644 --- a/oauth/OAuth2/Storage/Pdo.php +++ b/oauth/OAuth2/Storage/Pdo.php @@ -2,6 +2,10 @@ namespace OAuth2\Storage; +use OAuth2\OpenID\Storage\UserClaimsInterface; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; +use InvalidArgumentException; + /** * Simple PDO storage for all storage types * @@ -18,12 +22,30 @@ class Pdo implements AuthorizationCodeInterface, AccessTokenInterface, ClientCredentialsInterface, + UserCredentialsInterface, RefreshTokenInterface, - ScopeInterface + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface { + /** + * @var \PDO + */ protected $db; + + /** + * @var array + */ protected $config; + /** + * @param mixed $connection + * @param array $config + * + * @throws InvalidArgumentException + */ public function __construct($connection, $config = array()) { if (!$connection instanceof \PDO) { @@ -54,12 +76,20 @@ class Pdo implements 'access_token_table' => 'oauth_access_tokens', 'refresh_token_table' => 'oauth_refresh_tokens', 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', 'assoc_users_table' => 'users' ), $config); } - /* OAuth2\Storage\ClientCredentialsInterface */ + /** + * @param string $client_id + * @param null|string $client_secret + * @return bool + */ public function checkClientCredentials($client_id, $client_secret = null) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); @@ -70,6 +100,10 @@ class Pdo implements return $result && $result['client_secret'] == $client_secret; } + /** + * @param string $client_id + * @return bool + */ public function isPublicClient($client_id) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); @@ -82,7 +116,10 @@ class Pdo implements return empty($result['client_secret']); } - /* OAuth2\Storage\ClientInterface */ + /** + * @param string $client_id + * @return array|mixed + */ public function getClientDetails($client_id) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); @@ -91,19 +128,32 @@ class Pdo implements return $stmt->fetch(\PDO::FETCH_ASSOC); } - public function getClientScope($client_id) + /** + * @param string $client_id + * @param null|string $client_secret + * @param null|string $redirect_uri + * @param null|array $grant_types + * @param null|string $scope + * @param null|string $user_id + * @return bool + */ + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { - if (!$clientDetails = $this->getClientDetails($client_id)) { - return false; + // if it exists, update it. + if ($this->getClientDetails($client_id)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_secret=:client_secret, redirect_uri=:redirect_uri, grant_types=:grant_types, scope=:scope, user_id=:user_id where client_id=:client_id', $this->config['client_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (client_id, client_secret, redirect_uri, grant_types, scope, user_id) VALUES (:client_id, :client_secret, :redirect_uri, :grant_types, :scope, :user_id)', $this->config['client_table'])); } - if (isset($clientDetails['scope'])) { - return $clientDetails['scope']; - } - - return null; + return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')); } + /** + * @param $client_id + * @param $grant_type + * @return bool + */ public function checkRestrictedGrantType($client_id, $grant_type) { $details = $this->getClientDetails($client_id); @@ -117,7 +167,10 @@ class Pdo implements return true; } - /* OAuth2\Storage\AccessTokenInterface */ + /** + * @param string $access_token + * @return array|bool|mixed|null + */ public function getAccessToken($access_token) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); @@ -131,6 +184,14 @@ class Pdo implements return $token; } + /** + * @param string $access_token + * @param mixed $client_id + * @param mixed $user_id + * @param int $expires + * @param string $scope + * @return bool + */ public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) { // convert expires to datestring @@ -146,6 +207,10 @@ class Pdo implements return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); } + /** + * @param $access_token + * @return bool + */ public function unsetAccessToken($access_token) { $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE access_token = :access_token', $this->config['access_token_table'])); @@ -156,6 +221,10 @@ class Pdo implements } /* OAuth2\Storage\AuthorizationCodeInterface */ + /** + * @param string $code + * @return mixed + */ public function getAuthorizationCode($code) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); @@ -167,10 +236,26 @@ class Pdo implements } return $code; - } + } - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) + /** + * @param string $code + * @param mixed $client_id + * @param mixed $user_id + * @param string $redirect_uri + * @param int $expires + * @param string $scope + * @param string $id_token + * @return bool|mixed + */ + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) { + if (func_num_args() > 6) { + // we are calling with an id token + return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args()); + } + // convert expires to datestring $expires = date('Y-m-d H:i:s', $expires); @@ -184,6 +269,35 @@ class Pdo implements return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope')); } + /** + * @param string $code + * @param mixed $client_id + * @param mixed $user_id + * @param string $redirect_uri + * @param string $expires + * @param string $scope + * @param string $id_token + * @return bool + */ + private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table'])); + } + + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token')); + } + + /** + * @param string $code + * @return bool + */ public function expireAuthorizationCode($code) { $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table'])); @@ -191,8 +305,81 @@ class Pdo implements return $stmt->execute(compact('code')); } + /** + * @param string $username + * @param string $password + * @return bool + */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } - /* OAuth2\Storage\RefreshTokenInterface */ + return false; + } + + /** + * @param string $username + * @return array|bool + */ + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /** + * @param mixed $user_id + * @param string $claims + * @return array|bool + */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + /** + * @param string $claim + * @param array $userDetails + * @return array + */ + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /** + * @param string $refresh_token + * @return bool|mixed + */ public function getRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); @@ -206,6 +393,14 @@ class Pdo implements return $token; } + /** + * @param string $refresh_token + * @param mixed $client_id + * @param mixed $user_id + * @param string $expires + * @param string $scope + * @return bool + */ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { // convert expires to datestring @@ -216,6 +411,10 @@ class Pdo implements return $stmt->execute(compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope')); } + /** + * @param string $refresh_token + * @return bool + */ public function unsetRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); @@ -225,7 +424,71 @@ class Pdo implements return $stmt->rowCount() > 0; } - /* ScopeInterface */ + /** + * plaintext passwords are bad! Override this for your application + * + * @param array $user + * @param string $password + * @return bool + */ + protected function checkPassword($user, $password) + { + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); + } + + /** + * @param string $username + * @return array|bool + */ + public function getUser($username) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table'])); + $stmt->execute(array('username' => $username)); + + if (!$userInfo = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username + ), $userInfo); + } + + /** + * plaintext passwords are bad! Override this for your application + * + * @param string $username + * @param string $password + * @param string $firstName + * @param string $lastName + * @return bool + */ + public function setUser($username, $password, $firstName = null, $lastName = null) + { + // do not store in plaintext + $password = $this->hashPassword($password); + + // if it exists, update it. + if ($this->getUser($username)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET password=:password, first_name=:firstName, last_name=:lastName where username=:username', $this->config['user_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (username, password, first_name, last_name) VALUES (:username, :password, :firstName, :lastName)', $this->config['user_table'])); + } + + return $stmt->execute(compact('username', 'password', 'firstName', 'lastName')); + } + + /** + * @param string $scope + * @return bool + */ public function scopeExists($scope) { $scope = explode(' ', $scope); @@ -240,6 +503,10 @@ class Pdo implements return false; } + /** + * @param mixed $client_id + * @return null|string + */ public function getDefaultScope($client_id = null) { $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table'])); @@ -256,6 +523,123 @@ class Pdo implements return null; } + /** + * @param mixed $client_id + * @param $subject + * @return string + */ + public function getClientKey($client_id, $subject) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key from %s where client_id=:client_id AND subject=:subject', $this->config['jwt_table'])); + + $stmt->execute(array('client_id' => $client_id, 'subject' => $subject)); + + return $stmt->fetchColumn(); + } + + /** + * @param mixed $client_id + * @return bool|null + */ + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + /** + * @param mixed $client_id + * @param $subject + * @param $audience + * @param $expires + * @param $jti + * @return array|null + */ + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table'])); + + $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return array( + 'issuer' => $result['issuer'], + 'subject' => $result['subject'], + 'audience' => $result['audience'], + 'expires' => $result['expires'], + 'jti' => $result['jti'], + ); + } + + return null; + } + + /** + * @param mixed $client_id + * @param $subject + * @param $audience + * @param $expires + * @param $jti + * @return bool + */ + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (issuer, subject, audience, expires, jti) VALUES (:client_id, :subject, :audience, :expires, :jti)', $this->config['jti_table'])); + + return $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + } + + /** + * @param mixed $client_id + * @return mixed + */ + public function getPublicKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['public_key']; + } + } + + /** + * @param mixed $client_id + * @return mixed + */ + public function getPrivateKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['private_key']; + } + } + + /** + * @param mixed $client_id + * @return string + */ + public function getEncryptionAlgorithm($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['encryption_algorithm']; + } + + return 'RS256'; + } + /*-------------------------------------------------------------------------------------------------------------------------------------------------*/ /** * @author Denis CLAVIER @@ -306,10 +690,14 @@ class Pdo implements /*-------------------------------------------------------------------------------------------------------------------------------------------------*/ + /** * DDL to create OAuth2 database and tables for PDO storage * * @see https://github.com/dsquier/oauth2-server-php-mysql + * + * @param string $dbName + * @return string */ public function getBuildSql($dbName = 'oauth2_server_php') { @@ -324,46 +712,72 @@ class Pdo implements PRIMARY KEY (client_id) ); - CREATE TABLE {$this->config['access_token_table']} ( - access_token VARCHAR(40) NOT NULL, - client_id VARCHAR(80) NOT NULL, - user_id VARCHAR(80), - expires TIMESTAMP NOT NULL, - scope VARCHAR(4000), - PRIMARY KEY (access_token) - ); + CREATE TABLE {$this->config['access_token_table']} ( + access_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (access_token) + ); - CREATE TABLE {$this->config['code_table']} ( - authorization_code VARCHAR(40) NOT NULL, - client_id VARCHAR(80) NOT NULL, - user_id VARCHAR(80), - redirect_uri VARCHAR(2000), - expires TIMESTAMP NOT NULL, - scope VARCHAR(4000), - PRIMARY KEY (authorization_code) - ); + CREATE TABLE {$this->config['code_table']} ( + authorization_code VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + redirect_uri VARCHAR(2000), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + id_token VARCHAR(1000), + PRIMARY KEY (authorization_code) + ); - CREATE TABLE {$this->config['refresh_token_table']} ( - refresh_token VARCHAR(40) NOT NULL, - client_id VARCHAR(80) NOT NULL, - user_id VARCHAR(80), - expires TIMESTAMP NOT NULL, - scope VARCHAR(4000), - PRIMARY KEY (refresh_token) - ); + CREATE TABLE {$this->config['refresh_token_table']} ( + refresh_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (refresh_token) + ); - CREATE TABLE {$this->config['user_table']} ( - id SERIAL NOT NULL, - username VARCHAR(80) NOT NULL, - PRIMARY KEY (id) - ); + CREATE TABLE {$this->config['user_table']} ( + username VARCHAR(80), + password VARCHAR(80), + first_name VARCHAR(80), + last_name VARCHAR(80), + email VARCHAR(80), + email_verified BOOLEAN, + scope VARCHAR(4000) + ); - CREATE TABLE {$this->config['scope_table']} ( - scope VARCHAR(80) NOT NULL, - is_default BOOLEAN, - PRIMARY KEY (scope) - ); -"; + CREATE TABLE {$this->config['scope_table']} ( + scope VARCHAR(80) NOT NULL, + is_default BOOLEAN, + PRIMARY KEY (scope) + ); + + CREATE TABLE {$this->config['jwt_table']} ( + client_id VARCHAR(80) NOT NULL, + subject VARCHAR(80), + public_key VARCHAR(2000) NOT NULL + ); + + CREATE TABLE {$this->config['jti_table']} ( + issuer VARCHAR(80) NOT NULL, + subject VARCHAR(80), + audiance VARCHAR(80), + expires TIMESTAMP NOT NULL, + jti VARCHAR(2000) NOT NULL + ); + + CREATE TABLE {$this->config['public_key_table']} ( + client_id VARCHAR(80), + public_key VARCHAR(2000), + private_key VARCHAR(2000), + encryption_algorithm VARCHAR(100) DEFAULT 'RS256' + ) + "; return $sql; } diff --git a/oauth/OAuth2/Storage/PublicKeyInterface.php b/oauth/OAuth2/Storage/PublicKeyInterface.php new file mode 100644 index 0000000..a6dc49f --- /dev/null +++ b/oauth/OAuth2/Storage/PublicKeyInterface.php @@ -0,0 +1,30 @@ + + */ +interface PublicKeyInterface +{ + /** + * @param mixed $client_id + * @return mixed + */ + public function getPublicKey($client_id = null); + + /** + * @param mixed $client_id + * @return mixed + */ + public function getPrivateKey($client_id = null); + + /** + * @param mixed $client_id + * @return mixed + */ + public function getEncryptionAlgorithm($client_id = null); +} \ No newline at end of file diff --git a/oauth/OAuth2/Storage/Redis.php b/oauth/OAuth2/Storage/Redis.php new file mode 100644 index 0000000..e6294e2 --- /dev/null +++ b/oauth/OAuth2/Storage/Redis.php @@ -0,0 +1,321 @@ + + * $storage = new OAuth2\Storage\Redis($redis); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * + */ +class Redis implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /* The redis client */ + protected $redis; + + /* Configuration array */ + protected $config; + + /** + * Redis Storage! + * + * @param \Predis\Client $redis + * @param array $config + */ + public function __construct($redis, $config=array()) + { + $this->redis = $redis; + $this->config = array_merge(array( + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + ), $config); + } + + protected function getValue($key) + { + if ( isset($this->cache[$key]) ) { + return $this->cache[$key]; + } + $value = $this->redis->get($key); + if ( isset($value) ) { + return json_decode($value, true); + } else { + return false; + } + } + + protected function setValue($key, $value, $expire=0) + { + $this->cache[$key] = $value; + $str = json_encode($value); + if ($expire > 0) { + $seconds = $expire - time(); + $ret = $this->redis->setex($key, $seconds, $str); + } else { + $ret = $this->redis->set($key, $str); + } + + // check that the key was set properly + // if this fails, an exception will usually thrown, so this step isn't strictly necessary + return is_bool($ret) ? $ret : $ret->getPayload() == 'OK'; + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + + return $this->redis->del($key); + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $user = $this->getUserDetails($username); + + return $user && $user['password'] === $password; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($client['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + + return $result > 0; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetAccessToken($access_token) + { + $result = $this->expireValue($this->config['access_token_key'] . $access_token); + + return $result > 0; + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return null; + } + + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); + } +} diff --git a/oauth/OAuth2/Storage/UserCredentialsInterface.php b/oauth/OAuth2/Storage/UserCredentialsInterface.php new file mode 100644 index 0000000..f550579 --- /dev/null +++ b/oauth/OAuth2/Storage/UserCredentialsInterface.php @@ -0,0 +1,52 @@ + + */ +interface UserCredentialsInterface +{ + /** + * Grant access tokens for basic user credentials. + * + * Check the supplied username and password for validity. + * + * You can also use the $client_id param to do any checks required based + * on a client, if you need that. + * + * Required for OAuth2::GRANT_TYPE_USER_CREDENTIALS. + * + * @param $username + * Username to be check with. + * @param $password + * Password to be check with. + * + * @return + * TRUE if the username and password are valid, and FALSE if it isn't. + * Moreover, if the username and password are valid, and you want to + * + * @see http://tools.ietf.org/html/rfc6749#section-4.3 + * + * @ingroup oauth2_section_4 + */ + public function checkUserCredentials($username, $password); + + /** + * @param string $username - username to get details for + * @return array|false - the associated "user_id" and optional "scope" values + * This function MUST return FALSE if the requested user does not exist or is + * invalid. "scope" is a space-separated list of restricted scopes. + * @code + * return array( + * "user_id" => USER_ID, // REQUIRED user_id to be stored with the authorization code or access token + * "scope" => SCOPE // OPTIONAL space-separated list of restricted scopes + * ); + * @endcode + */ + public function getUserDetails($username); +} diff --git a/oauth/OAuth2/TokenType/Mac.php b/oauth/OAuth2/TokenType/Mac.php new file mode 100644 index 0000000..fe6a86a --- /dev/null +++ b/oauth/OAuth2/TokenType/Mac.php @@ -0,0 +1,22 @@ +validateAuthorizeRequest($request, $response)) { $response->send(); die; } -// if user is not yet authenticated, he is redirected. +// If user is not yet authenticated, he is redirected. if (!isset($_SESSION['uid'])) { - //store the authorize request - $explode_url=explode("/", strip_tags(trim($_SERVER['REQUEST_URI']))); + // Store the authorize request + $explode_url=explode("/", strip_tags(trim($_SERVER['REQUEST_URI']))); $_SESSION['auth_page']=end($explode_url); header('Location: index.php'); exit(); } - -// display an authorization form -if (empty($_POST)) { +// Check if user has already authorized oauth to share data with Mattermost. In this case, user should exist in 'user' table. +if ($server->userExists($_SESSION['uid'])) { + // Bypass authorize form, continue Oauth process. + $server->handleAuthorizeRequest($request, $response, true, $_SESSION['uid']); +} +// Display an authorization form +else if (empty($_POST)) { exit(' @@ -48,58 +57,58 @@ if (empty($_POST)) { - +
 
Mattermost desires access to your LDAP data:
- +
- + - - - + +
- +
- -
+ +  
- Login as : ' . $_SESSION['uid'] . ' + Login as : ' . $_SESSION['uid'] . '
- +
Requested Data :
  -> Username,
-   -> Full Name,
+   -> Full Name,
  -> Email - +
 
+
- +
- +
@@ -108,12 +117,13 @@ if (empty($_POST)) { '); } +else { + // Print the authorization code if the user has authorized your client + $is_authorized = ($_POST['authorized'] === 'Authorize'); + $server->handleAuthorizeRequest($request, $response, $is_authorized, $_SESSION['uid']); +} -// print the authorization code if the user has authorized your client -$is_authorized = ($_POST['authorized'] === 'Authorize'); -$server->handleAuthorizeRequest($request, $response, $is_authorized,$_SESSION['uid']); - -if ($is_authorized) +if ($is_authorized) { // This is only here so that you get to see your code in the cURL request. Otherwise, we'd redirect back to the client $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=')+5, 40); @@ -122,4 +132,4 @@ if ($is_authorized) } // Send message in case of error -$response->send(); \ No newline at end of file +$response->send(); diff --git a/oauth/config_db.php.example b/oauth/config_db.php.example index 0132c42..6589db2 100755 --- a/oauth/config_db.php.example +++ b/oauth/config_db.php.example @@ -1,14 +1,14 @@
'; - echo 'Click here to come back to login page'; -} -else -{ - // Check received data length (to prevent code injection) - if (strlen($_POST['user']) > 15) - { - echo 'Username has incorrect format ... Please try again

'; - echo 'Click here to come back to login page'; +// Verify all fields have been filled +if (empty($_POST['user']) || empty($_POST['password'])) { + echo 'Please fill in your Username and Password

'; + echo 'Click here to come back to login page'; +} else { + // Check received data length (to prevent code injection) + if (strlen($_POST['user']) > 15) { + echo 'Username has incorrect format ... Please try again

'; + echo 'Click here to come back to login page'; + } elseif (strlen($_POST['password']) > 50 || strlen($_POST['password']) <= 7) { + echo 'Password has incorrect format ... Please try again

'; + echo 'Click here to come back to login page'; + } else { + // Remove every html tag and useless space on username (to prevent XSS) + $user=strtolower(strip_tags(htmlspecialchars(trim($_POST['user'])))); + + $password=$_POST['password']; + + // Open a LDAP connection + $ldap = new LDAP($ldap_host, $ldap_port, $ldap_version); + + // Check user credential on LDAP + try { + $authenticated = $ldap->checkLogin($user, $password, $ldap_search_attribute, $ldap_filter, $ldap_base_dn, $ldap_bind_dn, $ldap_bind_pass); + } catch (Exception $e) { + $resp = json_encode(array("error" => "Impossible to get data", "message" => $e->getMessage())); + $authenticated = false; + } + + // If user is authenticated + if ($authenticated) { + $_SESSION['uid']=$user; + + // If user came here with an autorize request, redirect him to the authorize page. Else prompt a simple message. + if (isset($_SESSION['auth_page'])) { + $auth_page=$_SESSION['auth_page']; + header('Location: ' . $auth_page); + exit(); + } else { + echo "Congratulation you are authenticated !

However there is nothing to do here ..."; + } + } + // check login on LDAP has failed. Login and password were invalid or LDAP is unreachable + else { + echo "Authentication failed ... Check your username and password.
If error persist contact your administrator.

"; + echo 'Click here to come back to login page'; + echo '


' . $resp; + } } - elseif (strlen($_POST['password']) > 50 || strlen($_POST['password']) <= 7) - { - echo 'Password has incorrect format ... Please try again

'; - echo 'Click here to come back to login page'; - } - else - { - // Remove every html tag and useless space on username (to prevent XSS) - $user=strip_tags(trim($_POST['user'])); - - $user=$_POST['user']; - $password=$_POST['password']; - - // Open a LDAP connection - $ldap = new LDAP($ldap_host,$ldap_port,$ldap_version); - - // Check user credential on LDAP - try{ - $authenticated = $ldap->checkLogin($user,$password,$ldap_search_attribute,$ldap_filter,$ldap_base_dn,$ldap_bind_dn,$ldap_bind_pass); - } - catch (Exception $e) - { - $resp = json_encode(array("error" => "Impossible to get data", "message" => $e->getMessage())); - $authenticated = false; - } - - // If user is authenticated - if ($authenticated) - { - $_SESSION['uid']=$user; - - // If user came here with an autorize request, redirect him to the authorize page. Else prompt a simple message. - if (isset($_SESSION['auth_page'])) - { - $auth_page=$_SESSION['auth_page']; - header('Location: ' . $auth_page); - exit(); - } - else - { - echo "Congratulation you are authenticated !

However there is nothing to do here ..."; - } - } - // check login on LDAP has failed. Login and password were invalid or LDAP is unreachable - else - { - echo "Authentication failed ... Check your username and password.
If error persist contact your administrator.

"; - echo 'Click here to come back to login page'; - echo '


' . $resp; - } - } } diff --git a/oauth/resource.php b/oauth/resource.php index 59af40e..ebcd906 100755 --- a/oauth/resource.php +++ b/oauth/resource.php @@ -27,22 +27,19 @@ $user = $info_oauth["user_id"]; $assoc_id = intval($info_oauth["assoc_id"]); // Open a LDAP connection -$ldap = new LDAP($ldap_host,$ldap_port,$ldap_version); +$ldap = new LDAP($ldap_host, $ldap_port, $ldap_version); // Try to get user data on the LDAP -try -{ - $data = $ldap->getDataForMattermost($ldap_base_dn,$ldap_filter,$ldap_bind_dn,$ldap_bind_pass,$ldap_search_attribute,$user); +try { + $data = $ldap->getDataForMattermost($ldap_base_dn, $ldap_filter, $ldap_bind_dn, $ldap_bind_pass, $ldap_search_attribute, $user); - // Here is the patch for Mattermost 4.4 and older. Gitlab has changed the JSON output of oauth service. Many data are not used by Mattermost, but there is a stack error if we delete them. That's the reason why date and many parameters are null or empty. - $resp = array("id" => $assoc_id,"name" => $data['cn'],"username" => $user,"state" => "active","avatar_url" => "","web_url" => "","created_at" => "0000-00-00T00:00:00.000Z","bio" => null,"location" => null,"skype" => "","linkedin" => "","twitter" => "","website_url" => "","organization" => null,"last_sign_in_at" => "0000-00-00T00:00:00.000Z","confirmed_at" => "0000-00-00T00:00:00.000Z","last_activity_on" => null,"email" => $data['mail'],"theme_id" => 1,"color_scheme_id" => 1,"projects_limit" => 100000,"current_sign_in_at" => "0000-00-00T00:00:00.000Z","identities" => array(array("provider" => "ldapmain","extern_uid" => $data['dn'])),"can_create_group" => true,"can_create_project" => true,"two_factor_enabled" => false,"external" => false,"shared_runners_minutes_limit" => null); + // Here is the patch for Mattermost 4.4 and older. Gitlab has changed the JSON output of oauth service. Many data are not used by Mattermost, but there is a stack error if we delete them. That's the reason why date and many parameters are null or empty. + $resp = array("id" => $assoc_id,"name" => $data['cn'],"username" => $user,"state" => "active","avatar_url" => "","web_url" => "","created_at" => "0000-00-00T00:00:00.000Z","bio" => null,"location" => null,"skype" => "","linkedin" => "","twitter" => "","website_url" => "","organization" => null,"last_sign_in_at" => "0000-00-00T00:00:00.000Z","confirmed_at" => "0000-00-00T00:00:00.000Z","last_activity_on" => null,"email" => $data['mail'],"theme_id" => 1,"color_scheme_id" => 1,"projects_limit" => 100000,"current_sign_in_at" => "0000-00-00T00:00:00.000Z","identities" => array(array("provider" => "ldapmain","extern_uid" => $data['dn'])),"can_create_group" => true,"can_create_project" => true,"two_factor_enabled" => false,"external" => false,"shared_runners_minutes_limit" => null); - // Below is the old version, still consistent with Mattermost before version 4.4 - // $resp = array("name" => $data['cn'],"username" => $user,"id" => $assoc_id,"state" => "active","email" => $data['mail']); -} -catch (Exception $e) -{ - $resp = array("error" => "Impossible to get data", "message" => $e->getMessage()); + // Below is the old version, still consistent with Mattermost before version 4.4 + // $resp = array("name" => $data['cn'],"username" => $user,"id" => $assoc_id,"state" => "active","email" => $data['mail']); +} catch (Exception $e) { + $resp = array("error" => "Impossible to get data", "message" => $e->getMessage()); } // send data or error message in JSON format diff --git a/oauth/style.css b/oauth/style.css index 8b3765c..b7b06c6 100644 --- a/oauth/style.css +++ b/oauth/style.css @@ -12,25 +12,25 @@ body { text-decoration:none; background-color: white; height: 100%; - margin: 0; + margin: 0; } -.LoginTitle { +.LoginTitle { color: #000000; font-family : "Tahoma","Arial", serif; font-size : 18pt; font-weight: normal; } -.LoginUsername { +.LoginUsername { color: #000000; font-family : "Tahoma","Arial", serif; font-size : 14pt; font-weight: normal; } -.LoginComment { +.LoginComment { color: #000000; font-family : "Tahoma","Arial", serif; font-size : 8pt; @@ -53,5 +53,32 @@ body { color: Yellow; font-family : "Tahoma", "Arial", serif; font-size : 8pt; - font-weight: bold; + font-weight: bold; +} + +button { + overflow: visible; + width: auto; +} +button.link { + font-family: "Verdana" sans-serif; + font-size: 7pt; + text-align: left; + color: blue; + background: none; + margin: 0; + padding: 0; + border: none; + cursor: pointer; + + -moz-user-select: text; + + /* override all your button styles here if there are any others */ +} +button.link span { + text-decoration: underline; +} +button.link:hover span, +button.link:focus span { + color: black; } diff --git a/oauth/token.php b/oauth/token.php index f0ad755..fd21603 100644 --- a/oauth/token.php +++ b/oauth/token.php @@ -9,4 +9,4 @@ require_once __DIR__.'/server.php'; // Handle a request for an OAuth2.0 Access Token and send the response to the client $server->handleTokenRequest(OAuth2\Request::createFromGlobals())->send(); -?> \ No newline at end of file +?> From 01bb9289050f8b89c8cb1e4d619775d81d91b2e6 Mon Sep 17 00:00:00 2001 From: "Angus B. Grieve-Smith" Date: Fri, 1 May 2020 15:00:06 -0400 Subject: [PATCH 2/4] Fixed 'dn' bug and documentation in resource.php --- oauth/resource.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth/resource.php b/oauth/resource.php index ebcd906..0c48c4d 100755 --- a/oauth/resource.php +++ b/oauth/resource.php @@ -33,8 +33,8 @@ $ldap = new LDAP($ldap_host, $ldap_port, $ldap_version); try { $data = $ldap->getDataForMattermost($ldap_base_dn, $ldap_filter, $ldap_bind_dn, $ldap_bind_pass, $ldap_search_attribute, $user); - // Here is the patch for Mattermost 4.4 and older. Gitlab has changed the JSON output of oauth service. Many data are not used by Mattermost, but there is a stack error if we delete them. That's the reason why date and many parameters are null or empty. - $resp = array("id" => $assoc_id,"name" => $data['cn'],"username" => $user,"state" => "active","avatar_url" => "","web_url" => "","created_at" => "0000-00-00T00:00:00.000Z","bio" => null,"location" => null,"skype" => "","linkedin" => "","twitter" => "","website_url" => "","organization" => null,"last_sign_in_at" => "0000-00-00T00:00:00.000Z","confirmed_at" => "0000-00-00T00:00:00.000Z","last_activity_on" => null,"email" => $data['mail'],"theme_id" => 1,"color_scheme_id" => 1,"projects_limit" => 100000,"current_sign_in_at" => "0000-00-00T00:00:00.000Z","identities" => array(array("provider" => "ldapmain","extern_uid" => $data['dn'])),"can_create_group" => true,"can_create_project" => true,"two_factor_enabled" => false,"external" => false,"shared_runners_minutes_limit" => null); + // Here is the patch for Mattermost 4.4 and newer. Gitlab has changed the JSON output of oauth service. Many data are not used by Mattermost, but there is a stack error if we delete them. That's the reason why date and many parameters are null or empty. + $resp = array("id" => $assoc_id,"name" => $data['cn'],"username" => $user,"state" => "active","avatar_url" => "","web_url" => "","created_at" => "0000-00-00T00:00:00.000Z","bio" => null,"location" => null,"skype" => "","linkedin" => "","twitter" => "","website_url" => "","organization" => null,"last_sign_in_at" => "0000-00-00T00:00:00.000Z","confirmed_at" => "0000-00-00T00:00:00.000Z","last_activity_on" => null,"email" => $data['mail'],"theme_id" => 1,"color_scheme_id" => 1,"projects_limit" => 100000,"current_sign_in_at" => "0000-00-00T00:00:00.000Z","identities" => array(array("provider" => "ldapmain","extern_uid" => $data['cn'])),"can_create_group" => true,"can_create_project" => true,"two_factor_enabled" => false,"external" => false,"shared_runners_minutes_limit" => null); // Below is the old version, still consistent with Mattermost before version 4.4 // $resp = array("name" => $data['cn'],"username" => $user,"id" => $assoc_id,"state" => "active","email" => $data['mail']); From 3df8d4086adc716e8bfd20959687173b240bd45f Mon Sep 17 00:00:00 2001 From: "Angus B. Grieve-Smith" Date: Fri, 1 May 2020 15:05:46 -0400 Subject: [PATCH 3/4] Reformatted resource array for easier reading --- oauth/resource.php | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/oauth/resource.php b/oauth/resource.php index 0c48c4d..8cdd113 100755 --- a/oauth/resource.php +++ b/oauth/resource.php @@ -34,7 +34,41 @@ try { $data = $ldap->getDataForMattermost($ldap_base_dn, $ldap_filter, $ldap_bind_dn, $ldap_bind_pass, $ldap_search_attribute, $user); // Here is the patch for Mattermost 4.4 and newer. Gitlab has changed the JSON output of oauth service. Many data are not used by Mattermost, but there is a stack error if we delete them. That's the reason why date and many parameters are null or empty. - $resp = array("id" => $assoc_id,"name" => $data['cn'],"username" => $user,"state" => "active","avatar_url" => "","web_url" => "","created_at" => "0000-00-00T00:00:00.000Z","bio" => null,"location" => null,"skype" => "","linkedin" => "","twitter" => "","website_url" => "","organization" => null,"last_sign_in_at" => "0000-00-00T00:00:00.000Z","confirmed_at" => "0000-00-00T00:00:00.000Z","last_activity_on" => null,"email" => $data['mail'],"theme_id" => 1,"color_scheme_id" => 1,"projects_limit" => 100000,"current_sign_in_at" => "0000-00-00T00:00:00.000Z","identities" => array(array("provider" => "ldapmain","extern_uid" => $data['cn'])),"can_create_group" => true,"can_create_project" => true,"two_factor_enabled" => false,"external" => false,"shared_runners_minutes_limit" => null); + $resp = array( + "id" => $assoc_id, + "name" => $data['cn'], + "username" => $user, + "state" => "active", + "avatar_url" => "", + "web_url" => "", + "created_at" => "0000-00-00T00:00:00.000Z", + "bio" => null, + "location" => null, + "skype" => "", + "linkedin" => "", + "twitter" => "", + "website_url" => "", + "organization" => null, + "last_sign_in_at" => "0000-00-00T00:00:00.000Z", + "confirmed_at" => "0000-00-00T00:00:00.000Z", + "last_activity_on" => null, + "email" => $data['mail'], + "theme_id" => 1, + "color_scheme_id" => 1, + "projects_limit" => 100000, + "current_sign_in_at" => "0000-00-00T00:00:00.000Z", + "identities" => array( + array( + "provider" => "ldapmain", + "extern_uid" => $data['cn'] + ) + ), + "can_create_group" => true, + "can_create_project" => true, + "two_factor_enabled" => false, + "external" => false, + "shared_runners_minutes_limit" => null + ); // Below is the old version, still consistent with Mattermost before version 4.4 // $resp = array("name" => $data['cn'],"username" => $user,"id" => $assoc_id,"state" => "active","email" => $data['mail']); From a6fa56977562965da3bbfe0debded2a7cb7e4f2e Mon Sep 17 00:00:00 2001 From: "Angus B. Grieve-Smith" Date: Thu, 7 May 2020 17:53:58 -0400 Subject: [PATCH 4/4] Fix merge error in authorize.php --- oauth/authorize.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/oauth/authorize.php b/oauth/authorize.php index 717ee14..6de485c 100644 --- a/oauth/authorize.php +++ b/oauth/authorize.php @@ -90,11 +90,6 @@ else { // Check if user has authorized to share his data with the client $is_authorized = ($_POST['authorized'] === 'Authorize'); } -else { - // Print the authorization code if the user has authorized your client - $is_authorized = ($_POST['authorized'] === 'Authorize'); - $server->handleAuthorizeRequest($request, $response, $is_authorized, $_SESSION['uid']); -} // Print the authorization code if the user has authorized your client $server->handleAuthorizeRequest($request, $response, $is_authorized,$_SESSION['uid']);