From 7a9e6214de2fc08350e015e1c9bb91bfc1d76991 Mon Sep 17 00:00:00 2001 From: "yadong.zhang" Date: Tue, 30 Mar 2021 23:26:56 +0800 Subject: [PATCH] =?UTF-8?q?:alien:=20=E9=9B=86=E6=88=90=20Okta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOGS.md | 2 + .../zhyd/oauth/config/AuthDefaultSource.java | 35 +++- .../zhyd/oauth/enums/scope/AuthOktaScope.java | 66 ++++++++ .../zhyd/oauth/request/AuthOktaRequest.java | 159 ++++++++++++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 src/main/java/me/zhyd/oauth/enums/scope/AuthOktaScope.java create mode 100644 src/main/java/me/zhyd/oauth/request/AuthOktaRequest.java diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 6959e45..d4052a3 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -7,8 +7,10 @@ - 集成 Amazon 平台登录 - 集成 Slack 平台登录 - 集成 LINE 平台登录 + - 集成 Okta 平台登录 - 集成钉钉账号登录 - 修改 + - 【**重要**】 `AuthConfig`中的`codingGroupName`参数更名为`domainPrefix`,针对此类平台提供通用的配置。 - 修改 `AuthFacebookScope` 中的默认 scope,解决 justauth-demo 项目中使用 facebook 报错的问题 - 升级 facebook 的 api 到 v10.0 版本 - 优化部分代码 diff --git a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java index a914305..2e0f67f 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java @@ -942,5 +942,38 @@ public enum AuthDefaultSource implements AuthSource { public String revoke() { return "https://api.line.me/oauth2/v2.1/revoke"; } - } + }, + /** + * Okta, + *

+ * 团队/组织的域名不同,此处通过配置动态组装 + * + * @since 1.16.0 + */ + OKTA { + @Override + public String authorize() { + return "https://%s.okta.com/oauth2/%s/v1/authorize"; + } + + @Override + public String accessToken() { + return "https://%s.okta.com/oauth2/%s/v1/token"; + } + + @Override + public String refresh() { + return "https://%s.okta.com/oauth2/%s/v1/token"; + } + + @Override + public String userInfo() { + return "https://%s.okta.com/oauth2/%s/v1/userinfo"; + } + + @Override + public String revoke() { + return "https://%s.okta.com/oauth2/%s/v1/revoke"; + } + }, } diff --git a/src/main/java/me/zhyd/oauth/enums/scope/AuthOktaScope.java b/src/main/java/me/zhyd/oauth/enums/scope/AuthOktaScope.java new file mode 100644 index 0000000..087b135 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/enums/scope/AuthOktaScope.java @@ -0,0 +1,66 @@ +package me.zhyd.oauth.enums.scope; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Okta 平台 OAuth 授权范围 + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @since 1.16.0 + */ +@Getter +@AllArgsConstructor +public enum AuthOktaScope implements AuthScope { + + /** + * {@code scope} 含义,以{@code description} 为准 + */ + OPENID("openid", "Signals that a request is an OpenID request.", true), + PROFILE("profile", "The exact data varies based on what profile information you have provided, such as: name, time zone, picture, or birthday.", true), + EMAIL("email", "This allows the app to view your email address.", true), + ADDRESS("address", "This allows the app to view your address, such as: street address, city, state, and zip code.", true), + PHONE("phone", "This allows the app to view your phone number.", true), + OFFLINE_ACCESS("offline_access", "This keeps you signed in to the app, even when you are not using it.", true), + OKTA_USERS_MANAGE("okta.users.manage", "Allows the app to create and manage users and read all profile and credential information for users", false), + OKTA_USERS_READ("okta.users.read", "Allows the app to read any user's profile and credential information", false), + OKTA_USERS_MANAGE_SELF("okta.users.manage.self", "Allows the app to manage the currently signed-in user's profile. Currently only supports user profile attribute updates.", false), + OKTA_USERS_READ_SELF("okta.users.read.self", "Allows the app to read the currently signed-in user's profile and credential information", false), + OKTA_APPS_MANAGE("okta.apps.manage", "Allows the app to create and manage Apps in your Okta organization", false), + OKTA_APPS_READ("okta.apps.read", "Allows the app to read information about Apps in your Okta organization", false), + OKTA_AUTHORIZATIONSERVERS_MANAGE("okta.authorizationServers.manage", "Allows the app to manage authorization servers", false), + OKTA_AUTHORIZATIONSERVERS_READ("okta.authorizationServers.read", "Allows the app to read authorization server information", false), + OKTA_CLIENTS_MANAGE("okta.clients.manage", "Allows the app to manage all OAuth/OIDC clients and to create new clients", false), + OKTA_CLIENTS_READ("okta.clients.read", "Allows the app to read information for all OAuth/OIDC clients", false), + OKTA_CLIENTS_REGISTER("okta.clients.register", "Allows the app to register (create) new OAuth/OIDC clients (but not read information about existing clients)", false), + OKTA_EVENTHOOKS_MANAGE("okta.eventHooks.manage", "Allows the app to create and manage Event Hooks in your Okta organization", false), + OKTA_EVENTHOOKS_READ("okta.eventHooks.read", "Allows the app to read information about Event Hooks in your Okta organization", false), + OKTA_FACTORS_MANAGE("okta.factors.manage", "Allows the app to manage all admin operations for org factors (for example, activate, deactive, read)", false), + OKTA_FACTORS_READ("okta.factors.read", "Allows the app to read org factors information", false), + OKTA_GROUPS_MANAGE("okta.groups.manage", "Allows the app to manage groups in your Okta organization", false), + OKTA_GROUPS_READ("okta.groups.read", "Allows the app to read information about groups and their members in your Okta organization", false), + OKTA_IDPS_MANAGE("okta.idps.manage", "Allows the app to create and manage Identity Providers in your Okta organization", false), + OKTA_IDPS_READ("okta.idps.read", "Allows the app to read information about Identity Providers in your Okta organization", false), + OKTA_INLINEHOOKS_MANAGE("okta.inlineHooks.manage", "Allows the app to create and manage Inline Hooks in your Okta organization.", false), + OKTA_INLINEHOOKS_READ("okta.inlineHooks.read", "Allows the app to read information about Inline Hooks in your Okta organization.", false), + OKTA_LINKEDOBJECTS_MANAGE("okta.linkedObjects.manage", "Allows the app to manage Linked Object definitions in your Okta organization.", false), + OKTA_LINKEDOBJECTS_READ("okta.linkedObjects.read", "Allows the app to read Linked Object definitions in your Okta organization.", false), + OKTA_LOGS_READ("okta.logs.read", "Allows the app to read information about System Log entries in your Okta organization", false), + OKTA_ROLES_MANAGE("okta.roles.manage", "Allows the app to create and manage Administrator Roles in your Okta organization", false), + OKTA_ROLES_READ("okta.roles.read", "Allows the app to read information about Administrator Roles in your Okta organization", false), + OKTA_SCHEMAS_MANAGE("okta.schemas.manage", "Allows the app to create and manage Schemas in your Okta organization", false), + OKTA_SCHEMAS_READ("okta.schemas.read", "Allows the app to read information about Schemas in your Okta organization", false), + OKTA_SESSIONS_MANAGE("okta.sessions.manage", "Allows the app to manage all sessions in your Okta organization", false), + OKTA_SESSIONS_READ("okta.sessions.read", "Allows the app to read all sessions in your Okta organization", false), + OKTA_TEMPLATES_MANAGE("okta.templates.manage", "Allows the app to manage all custom templates in your Okta organization", false), + OKTA_TEMPLATES_READ("okta.templates.read", "Allows the app to read all custom templates in your Okta organization", false), + OKTA_TRUSTEDORIGINS_MANAGE("okta.trustedOrigins.manage", "Allows the app to manage all Trusted Origins in your Okta organization", false), + OKTA_TRUSTEDORIGINS_READ("okta.trustedOrigins.read", "Allows the app to read all Trusted Origins in your Okta organization", false), + OKTA_POLICIES_MANAGE("okta.policies.manage", "Allows the app to manage Policies in your Okta organization", false), + OKTA_POLICIES_READ("okta.policies.read", "Allows the app to read information about Policies in your Okta organization", false),; + + private final String scope; + private final String description; + private final boolean isDefault; + +} diff --git a/src/main/java/me/zhyd/oauth/request/AuthOktaRequest.java b/src/main/java/me/zhyd/oauth/request/AuthOktaRequest.java new file mode 100644 index 0000000..7c1e810 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthOktaRequest.java @@ -0,0 +1,159 @@ +package me.zhyd.oauth.request; + +import com.alibaba.fastjson.JSONObject; +import com.xkcoding.http.support.HttpHeader; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthDefaultSource; +import me.zhyd.oauth.enums.AuthResponseStatus; +import me.zhyd.oauth.enums.AuthUserGender; +import me.zhyd.oauth.enums.scope.AuthOktaScope; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.utils.AuthScopeUtils; +import me.zhyd.oauth.utils.Base64Utils; +import me.zhyd.oauth.utils.HttpUtils; +import me.zhyd.oauth.utils.UrlBuilder; + +import java.util.HashMap; +import java.util.Map; + +/** + * Okta 登录 + *

+ * https://{domainPrefix}.okta.com/oauth2/default/.well-known/oauth-authorization-server + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @since 1.16.0 + */ +public class AuthOktaRequest extends AuthDefaultRequest { + + public AuthOktaRequest(AuthConfig config) { + super(config, AuthDefaultSource.OKTA); + } + + public AuthOktaRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthDefaultSource.OKTA, authStateCache); + } + + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + String tokenUrl = accessTokenUrl(authCallback.getCode()); + return getAuthToken(tokenUrl); + } + + private AuthToken getAuthToken(String tokenUrl) { + HttpHeader header = new HttpHeader() + .add("accept", "application/json") + .add("content-type", "application/x-www-form-urlencoded") + .add("Authorization", "Basic " + Base64Utils.encode(config.getClientId().concat(":").concat(config.getClientSecret()))); + String response = new HttpUtils(config.getHttpConfig()).post(tokenUrl, null, header, false); + JSONObject accessTokenObject = JSONObject.parseObject(response); + this.checkResponse(accessTokenObject); + return AuthToken.builder() + .accessToken(accessTokenObject.getString("access_token")) + .tokenType(accessTokenObject.getString("token_type")) + .expireIn(accessTokenObject.getIntValue("expires_in")) + .scope(accessTokenObject.getString("scope")) + .refreshToken(accessTokenObject.getString("refresh_token")) + .idToken(accessTokenObject.getString("id_token")) + .build(); + } + + @Override + public AuthResponse refresh(AuthToken authToken) { + if (null == authToken.getRefreshToken()) { + return AuthResponse.builder() + .code(AuthResponseStatus.ILLEGAL_TOKEN.getCode()) + .msg(AuthResponseStatus.ILLEGAL_TOKEN.getMsg()) + .build(); + } + String refreshUrl = refreshTokenUrl(authToken.getRefreshToken()); + return AuthResponse.builder() + .code(AuthResponseStatus.SUCCESS.getCode()) + .data(this.getAuthToken(refreshUrl)) + .build(); + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + HttpHeader header = new HttpHeader() + .add("Authorization", "Bearer " + authToken.getAccessToken()); + String response = new HttpUtils(config.getHttpConfig()).post(userInfoUrl(authToken), null, header, false); + JSONObject object = JSONObject.parseObject(response); + this.checkResponse(object); + JSONObject address = object.getJSONObject("address"); + return AuthUser.builder() + .rawUserInfo(object) + .uuid(object.getString("sub")) + .username(object.getString("name")) + .nickname(object.getString("nickname")) + .email(object.getString("email")) + .location(null == address ? null : address.getString("street_address")) + .gender(AuthUserGender.getRealGender(object.getString("sex"))) + .token(authToken) + .source(source.toString()) + .build(); + } + + @Override + public AuthResponse revoke(AuthToken authToken) { + Map params = new HashMap<>(4); + params.put("token", authToken.getAccessToken()); + params.put("token_type_hint", "access_token"); + + HttpHeader header = new HttpHeader() + .add("Authorization", "Basic " + Base64Utils.encode(config.getClientId().concat(":").concat(config.getClientSecret()))); + new HttpUtils(config.getHttpConfig()).post(revokeUrl(authToken), params, header, false); + AuthResponseStatus status = AuthResponseStatus.SUCCESS; + return AuthResponse.builder().code(status.getCode()).msg(status.getMsg()).build(); + } + + private void checkResponse(JSONObject object) { + if (object.containsKey("error")) { + throw new AuthException(object.getString("error_description")); + } + } + + @Override + public String authorize(String state) { + return UrlBuilder.fromBaseUrl(String.format(source.authorize(), config.getDomainPrefix(), config.getAuthServerId())) + .queryParam("response_type", "code") + .queryParam("prompt", "consent") + .queryParam("client_id", config.getClientId()) + .queryParam("redirect_uri", config.getRedirectUri()) + .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthOktaScope.values()))) + .queryParam("state", getRealState(state)) + .build(); + } + + @Override + public String accessTokenUrl(String code) { + return UrlBuilder.fromBaseUrl(String.format(source.accessToken(), config.getDomainPrefix(), config.getAuthServerId())) + .queryParam("code", code) + .queryParam("grant_type", "authorization_code") + .queryParam("redirect_uri", config.getRedirectUri()) + .build(); + } + + @Override + protected String refreshTokenUrl(String refreshToken) { + return UrlBuilder.fromBaseUrl(String.format(source.refresh(), config.getDomainPrefix(), config.getAuthServerId())) + .queryParam("refresh_token", refreshToken) + .queryParam("grant_type", "refresh_token") + .build(); + } + + @Override + protected String revokeUrl(AuthToken authToken) { + return String.format(source.revoke(), config.getDomainPrefix(), config.getAuthServerId()); + } + + @Override + public String userInfoUrl(AuthToken authToken) { + return String.format(source.userInfo(), config.getDomainPrefix(), config.getAuthServerId()); + } +} -- GitLab